Build your own React

构建 简化版 react

参考: Build your own React

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body>
    <button id="btn">Update</button>
    <div id="root"></div>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <script type="text/babel">
      function createElement(type, props, ...children) {
        return {
          type,
          props: {
            ...props,
            children: children.map((child) => {
              return typeof child === "object"
                ? child
                : createTextElement(child);
            }),
          },
        };
      }

      function createTextElement(text) {
        return {
          type: "TEXT_ELEMENT",
          props: {
            nodeValue: text,
            children: [],
          },
        };
      }

      function createDom(fiber) {
        const dom =
          fiber.type === "TEXT_ELEMENT"
            ? document.createTextNode("")
            : document.createElement(fiber.type);
        updateDom(dom, {}, fiber.props);
        return dom;
      }

      const isEvent = (key) => key.startsWith("on");
      const isProperty = (key) => key !== "children";
      const isNew = (prev, next) => (key) => prev[key] !== next[key];
      const isGone = (prev, next) => (key) => !(key in next);
      function updateDom(dom, prevProps, nextProps) {
        Object.keys(prevProps)
          .filter(isEvent)
          .filter(
            (key) => !(key in nextProps) || isNew(prevProps, nextProps)(key)
          )
          .forEach((name) => {
            const eventType = name.toLowerCase().substring(2);
            dom.removeEventListener(eventType, prevProps[name]);
          });
        Object.keys(prevProps)
          .filter(isProperty)
          .filter(isGone(prevProps, nextProps))
          .forEach((name) => {
            dom[name] = "";
          });
        Object.keys(nextProps)
          .filter(isProperty)
          .filter(isNew(prevProps, nextProps))
          .forEach((name) => {
            dom[name] = nextProps[name];
          });
        Object.keys(nextProps)
          .filter(isEvent)
          .filter(isNew(prevProps, nextProps))
          .forEach((name) => {
            const eventType = name.toLocaleLowerCase().substring(2);
            dom.addEventListener(eventType, nextProps[name]);
          });
      }

      function commitRoot() {
        deletions.forEach(commitWork);
        commitWork(wipRoot.child);
        currentRoot = wipRoot;
        wipRoot = null;
      }

      function commitWork(fiber) {
        if (!fiber) return;
        let domParentFiber = fiber.parent;
        while (!domParentFiber.dom) {
          domParentFiber = domParentFiber.parent;
        }
        const domParent = domParentFiber.dom;
        if (fiber.effectTag === "PLACEMENT" && fiber.dom !== null) {
          domParent.appendChild(fiber.dom);
        } else if (fiber.effectTag === "UPDATE" && fiber.dom !== null) {
          updateDom(fiber.dom, fiber.alternate.props, fiber.props);
        } else if (fiber.effectTag === "DELETION") {
          commitDeletion(fiber, domParent);
        }
        commitWork(fiber.child);
        commitWork(fiber.sibling);
      }

      function commitDeletion(fiber, domParent) {
        if (fiber.dom) {
          domParent.removeChild(fiber.dom);
        } else {
          commitDeletion(fiber.child, domParent);
        }
      }

      function render(element, container) {
        wipRoot = {
          dom: container,
          props: {
            children: [element],
          },
          alternate: currentRoot,
        };
        deletions = [];
        nextUnitOfWork = wipRoot;
      }

      let nextUnitOfWork = null;
      let currentRoot = null;
      let wipRoot = null;
      let deletions = null;

      function workLoop(deadline) {
        let shouldYield = false;
        while (nextUnitOfWork && !shouldYield) {
          nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
          shouldYield = deadline.timeRemaining() < 1;
        }
        requestIdleCallback(workLoop);
        if (!nextUnitOfWork && wipRoot) {
          commitRoot();
        }
      }

      requestIdleCallback(workLoop);

      function performUnitOfWork(fiber) {
        const isFunctionComponent = fiber.type instanceof Function;
        if (isFunctionComponent) {
          updateFunctionComponent(fiber);
        } else {
          updateHostComponent(fiber);
        }

        if (fiber.child) return fiber.child;
        let nextFiber = fiber;
        while (nextFiber) {
          if (nextFiber.sibling) return nextFiber.sibling;
          nextFiber = nextFiber.parent;
        }
      }

      let wipFiber = null;
      let hookIndex = null;
      function updateFunctionComponent(fiber) {
        wipFiber = fiber;
        hookIndex = 0;
        wipFiber.hooks = [];
        const children = [fiber.type(fiber.props)];
        reconcileChildren(fiber, children);
      }

      function useState(initial) {
        const oldHook =
          wipFiber.alternate &&
          wipFiber.alternate.hooks &&
          wipFiber.alternate.hooks[hookIndex];
        // 从alternate中取出上一次的hook.
        const hook = {
          state: oldHook ? oldHook.state : initial,
          queue: [],
        };

        const actions = oldHook ? oldHook.queue : [];
        actions.forEach((action) => {
          hook.state = action(hook.state);
        });

        const setState = (action) => {
          hook.queue.push(action);
          (wipRoot = {
            dom: currentRoot.dom,
            props: currentRoot.props,
            alternate: currentRoot,
          }),
            (nextUnitOfWork = wipRoot);
          deletions = [];
        };
        wipFiber.hooks.push(hook);
        hookIndex++;
        return [hook.state, setState];
      }

      function updateHostComponent(fiber) {
        if (!fiber.dom) fiber.dom = createDom(fiber);
        reconcileChildren(fiber, fiber.props.children);
      }

      function reconcileChildren(wipFiber, elements) {
        let index = 0;
        let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
        let prevSibling;

        while (index < elements.length || oldFiber != null) {
          const element = elements[index];
          let newFiber = null;
          // compare oldFiber to element
          const sameType =
            oldFiber && element && element.type === oldFiber.type;
          if (sameType) {
            // Update the node
            newFiber = {
              type: oldFiber.type,
              props: element.props,
              dom: oldFiber.dom,
              parent: wipFiber,
              alternate: oldFiber,
              effectTag: "UPDATE",
            };
          }
          if (element && !sameType) {
            // Add this node
            newFiber = {
              type: element.type,
              props: element.props,
              dom: null,
              parent: wipFiber,
              alternate: null,
              effectTag: "PLACEMENT",
            };
          }
          if (oldFiber && !sameType) {
            // Delete the oldFiber's node
            oldFiber.effectTag = "DELETION";
            deletions.push(oldFiber);
          }
          if (oldFiber) oldFiber = oldFiber.sibling;

          if (index === 0) {
            wipFiber.child = newFiber;
          } else if (element) {
            prevSibling.sibling = newFiber;
          }
          prevSibling = newFiber;
          index++;
        }
      }

      const Didact = {
        createElement,
        render,
        useState,
      };

      /** @jsx Didact.createElement */
      function Counter() {
        const [state, setState] = Didact.useState(1);
        const [num, setNum] = Didact.useState(1);
        return (
          <div>
            <h1 onClick={() => setState((c) => c + 1)}>Count: {state}</h1>
            <h1 onClick={() => setNum((c) => c + 1)}>Num: {num}</h1>
          </div>
        );
      }
      const container = document.getElementById("root");
      Didact.render(<Counter></Counter>, container);
    </script>
  </body>
</html>

posted @ 2022-02-01 10:52  张润昊  阅读(58)  评论(0编辑  收藏  举报