# 总结

注意本文是教学性质的,还缺少很多 React 的功能和性能优化。

比如:在这些事情上 React 的表现和 Redact 不同。

  • Redact 在渲染阶段遍历了整棵树,而 React 用了一些启发性算法,可以直接跳过某些没有变化的子树,以提高性能。(比如 React 数组元素推荐带 key,可以跳过无需更新的节点,参考官方文档)
  • Redact 在 commit 阶段遍历整棵树, React 用了一个链表保存变化了的 fiber,减少了很多不必要遍历操作。
  • Redact 每次创建新的 fiber 树时都是直接创建 fiber 对象节点,而 React 会复用上一个 fiber 对象,以节省创建对象的性能消耗。
  • Redact 如果在渲染阶段收到新的更新会直接丢弃已渲染的树,再从头开始渲染。而 React 会用时间戳标记每次更新,以决定更新的优先级。
  • 源码还有很多优化等待读者去发现。。。

最终代码实现如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Redact</title>
    <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
</head>
<body>
    <div id="root"></div>
    <script type="text/babel">
      let nextUnitOfWork = null;
      let wipRoot = null;
      let currentRoot = null;
      let deletions = null;
      let wipFiber = null;
      let hookIndex = null;

      const isEvent = key => key.startsWith('on');
      const isProperty = key => key !== 'children' && !isEvent(key);
      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 => {
                  if (name === 'style') {
                      Object.entries(nextProps[name]).forEach(([key, value]) => {
                          dom.style[key] = value;
                      });
                  } else {
                      dom[name] = nextProps[name];
                  }
              });

          Object.keys(nextProps)
              .filter(isEvent)
              .filter(isNew(prevProps, nextProps))
              .forEach(name => {
                  const eventType = name.toLowerCase().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 createDom(fiber) {
          const dom = fiber.type === 'TEXT_ELEMENT'
              ? document.createTextNode('')
              : document.createElement(fiber.type);

          updateDom(dom, {}, fiber.props);

          return dom;
      }

      function createElement(type, props, ...children) {
          return {
              type,
              props: {
                  ...props,
                  children: children.map(child =>
                      typeof child === 'object'
                          ? child
                          : createTextElement(child)
                  )
              }
          };
      }

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

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

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

          if (!nextUnitOfWork && wipRoot) {
              commitRoot();
          }

          requestIdleCallback(workLoop);
      }

      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;
          }
      }

      function reconcileChildren(wipFiber, elements) {
          let index = 0;

          let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
          let prevSibling = null;

          elements = elements.flat();

          while (index < elements.length || oldFiber != null) {
              const element = elements[index];
              let newFiber = null;

              const sameType = oldFiber && element && element.type === oldFiber.type;

              if (sameType) {
                  newFiber = {
                      type: oldFiber.type,
                      props: element.props,
                      dom: oldFiber.dom,
                      parent: wipFiber,
                      alternate: oldFiber,
                      effectTag: 'UPDATE'
                  };
              }

              if (element && !sameType) {
                  newFiber = {
                      type: element.type,
                      props: element.props,
                      dom: null,
                      parent: wipFiber,
                      alternate: null,
                      effectTag: 'PLACEMENT'
                  };
              }

              if (oldFiber && !sameType) {
                  oldFiber.effectTag = 'DELETION';

                  deletions.push(oldFiber);
              }

              if (oldFiber) {
                  oldFiber = oldFiber.sibling;
              }

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

              prevSibling = newFiber;
              index++;
          }
      }

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

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

      function useState(initial) {
        const oldHook =
          wipFiber.alternate
          && wipFiber.alternate.hooks
          && wipFiber.alternate.hooks[hookIndex];
        const hook = {
          state: oldHook ? oldHook.state : initial,
          queue: []
        };

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

        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];
      }

      const Redact = {
          createElement,
          createTextElement,
          render,
          useState
      };
      /** @jsx Redact.createElement */
      function Counter() {
        const [state, setState] = Redact.useState(1);
        return (
          <h1 onClick={() => setState(c => c + 1)}>
            Count: {state}
          </h1>
        );
      }
      const element = <Counter />;
      const container = document.getElementById("root");
      Redact.render(element, container)
  </script>
</body>
</html>
最近更新时间: 2020/5/26 19:03:58