快来看 React 渲染过程(上)

157 阅读4分钟

背景

作为一名使用 React 多年(两年)的前端工程师~~(切图仔)~~ 常常会因为搞不清楚 React 内部的渲染原理而编写出意料之外的代码,导致代码出现莫名其妙的问题,于是下定决心,利用自己的空闲时间来解开 React 内部的秘密~

前置知识

双缓存

从 React16 以后,React 的架构就变成了 Fiber 架构,得益于 Fiber 架构,其实 React 内部的做法是在我们当前的屏幕上展示的是 currentFiber,通过 currentFiber 渲染出真实的DOM 节点,而存在于内存中的是 WorkProgressFiber,这棵树是在内存中构建的。当这棵树构建完毕之后,就会两棵树位置互换,达到重新渲染的目的。

原理

先把整个执行过程用流程图的方式体现出来吧

image.png

过程

下面结合一个具体的JSX 的组件来看整个 React的调度过程。

demo

const Demo2 = () => {
  return (
    <>
      <div>
        测试
        <button>
          123
        </button>
      </div>
    </>
  );
};

准备阶段

  1. React 初始化阶段会先创建两个关键对象 FiberRoot(代码里的 root) 和 HostFiber(代码里的uninitializedFiber),将 FiberRoot通过 current 和 HostFiber 相连,HostFiber 通过 stateNode 连接 FiberRoot。
 function createFiberRoot(
      containerInfo,
      tag,
      hydrate,
      hydrationCallbacks,
      isStrictMode,
      concurrentUpdatesByDefaultOverride
    ) {
      / ...
      var root = new FiberRootNode(containerInfo, tag, hydrate);
      // stateNode is any.

      var uninitializedFiber = createHostRootFiber(tag, isStrictMode);
      root.current = uninitializedFiber;
      uninitializedFiber.stateNode = root;
      /...
      return root;
    }

image.png

  1. 接着进入 Render 函数,Render 函数主要是启动了一个调度器去调度我们当前的任务,直接看代码
    function updateContainer(element, container, parentComponent, callback) {
      

      / ...
      var root = scheduleUpdateOnFiber(current$1, lane, eventTime);

      / ... 
      return lane;
    }
  1. scheduleUpdateOnFiber是具体的调度过程,这里不展开讲,后续会出一篇文章单独分析,现在简单理解为他作为一个调度者派发一个执行者去执行我们当前需要渲染的任务。
  2. 进入真正生成 WorkInProgress 树的过程,主要就是一个循环,通过判断workInProgress 是否有值来进行调用performUnitOfWork,如果 workInProgress 值为 null,代表内存中树生成完毕,结束循环。
function workLoopSync() {
      // Already timed out, so perform work without checking if we need to yield.
      while (workInProgress !== null) {
        performUnitOfWork(workInProgress);
      }
    }
  1. performUnitOfWork方法主要做了两件事情,一个是通过 beiginWork, 将 Babel 解析出来的虚拟 dom 生成 Fiber 节点,并将生成出来的节点和已有节点进行绑定。同时如果 fiber 节点有副作用的话,还会打上副作用标记。另一个是通过completeUnitOfWork,将 Fiber 节点中填充对应的真实 dom,并将 flags 进行冒泡。
function performUnitOfWork(unitOfWork) {
      // The current, flushed, state of this fiber is the alternate. Ideally
      // nothing should rely on this, but relying on it here means that we don't
      // need an additional field on the work in progress.
      var current = unitOfWork.alternate;
      setCurrentFiber(unitOfWork);
      var next;

      if ((unitOfWork.mode & ProfileMode) !== NoMode) {
        startProfilerTimer(unitOfWork);
        next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
        stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
      } else {
        next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
      }

      resetCurrentFiber();
      unitOfWork.memoizedProps = unitOfWork.pendingProps;

      if (next === null) {
        // If this doesn't spawn new work, complete the current work.
        completeUnitOfWork(unitOfWork);
      } else {
        workInProgress = next;
      }

      ReactCurrentOwner$2.current = null;
    }

beiginWork

    function beginWork(current, workInProgress, renderLanes) {
      / ...
      switch (workInProgress.tag) {
        case IndeterminateComponent: {
          return mountIndeterminateComponent(
            current,
            workInProgress,
            workInProgress.type,
            renderLanes
          );
        }

        case LazyComponent: {
          var elementType = workInProgress.elementType;
          return mountLazyComponent(
            current,
            workInProgress,
            elementType,
            renderLanes
          );
        }

        case FunctionComponent: {
          var Component = workInProgress.type;
          var unresolvedProps = workInProgress.pendingProps;
          var resolvedProps =
            workInProgress.elementType === Component
              ? unresolvedProps
              : resolveDefaultProps(Component, unresolvedProps);
          return updateFunctionComponent(
            current,
            workInProgress,
            Component,
            resolvedProps,
            renderLanes
          );
        }

        case ClassComponent: {
          var _Component = workInProgress.type;
          var _unresolvedProps = workInProgress.pendingProps;

          var _resolvedProps =
            workInProgress.elementType === _Component
              ? _unresolvedProps
              : resolveDefaultProps(_Component, _unresolvedProps);

          return updateClassComponent(
            current,
            workInProgress,
            _Component,
            _resolvedProps,
            renderLanes
          );
        }

        case HostRoot:
          return updateHostRoot(current, workInProgress, renderLanes);

        case HostComponent:
          return updateHostComponent$1(current, workInProgress, renderLanes);

        case HostText:
          return updateHostText$1(current, workInProgress);

        case SuspenseComponent:
          return updateSuspenseComponent(current, workInProgress, renderLanes);

        case HostPortal:
          return updatePortalComponent(current, workInProgress, renderLanes);

        case ForwardRef: {
          var type = workInProgress.type;
          var _unresolvedProps2 = workInProgress.pendingProps;

          var _resolvedProps2 =
            workInProgress.elementType === type
              ? _unresolvedProps2
              : resolveDefaultProps(type, _unresolvedProps2);

          return updateForwardRef(
            current,
            workInProgress,
            type,
            _resolvedProps2,
            renderLanes
          );
        }

        case Fragment:
          return updateFragment(current, workInProgress, renderLanes);

        case Mode:
          return updateMode(current, workInProgress, renderLanes);

        case Profiler:
          return updateProfiler(current, workInProgress, renderLanes);

        case ContextProvider:
          return updateContextProvider(current, workInProgress, renderLanes);

        case ContextConsumer:
          return updateContextConsumer(current, workInProgress, renderLanes);

        case MemoComponent: {
          var _type2 = workInProgress.type;
          var _unresolvedProps3 = workInProgress.pendingProps; // Resolve outer props first, then resolve inner props.

          var _resolvedProps3 = resolveDefaultProps(_type2, _unresolvedProps3);

          {
            if (workInProgress.type !== workInProgress.elementType) {
              var outerPropTypes = _type2.propTypes;

              if (outerPropTypes) {
                checkPropTypes(
                  outerPropTypes,
                  _resolvedProps3, // Resolved for outer only
                  "prop",
                  getComponentNameFromType(_type2)
                );
              }
            }
          }

          _resolvedProps3 = resolveDefaultProps(_type2.type, _resolvedProps3);
          return updateMemoComponent(
            current,
            workInProgress,
            _type2,
            _resolvedProps3,
            renderLanes
          );
        }

        case SimpleMemoComponent: {
          return updateSimpleMemoComponent(
            current,
            workInProgress,
            workInProgress.type,
            workInProgress.pendingProps,
            renderLanes
          );
        }

        case IncompleteClassComponent: {
          var _Component2 = workInProgress.type;
          var _unresolvedProps4 = workInProgress.pendingProps;

          var _resolvedProps4 =
            workInProgress.elementType === _Component2
              ? _unresolvedProps4
              : resolveDefaultProps(_Component2, _unresolvedProps4);

          return mountIncompleteClassComponent(
            current,
            workInProgress,
            _Component2,
            _resolvedProps4,
            renderLanes
          );
        }

        case SuspenseListComponent: {
          return updateSuspenseListComponent(
            current,
            workInProgress,
            renderLanes
          );
        }

        case ScopeComponent: {
          break;
        }

        case OffscreenComponent: {
          return updateOffscreenComponent(current, workInProgress, renderLanes);
        }

        case LegacyHiddenComponent: {
          return updateLegacyHiddenComponent(
            current,
            workInProgress,
            renderLanes
          );
        }
      }

      throw new Error(
        "Unknown unit of work tag (" +
          workInProgress.tag +
          "). This error is likely caused by a bug in " +
          "React. Please file an issue."
      );
    }

代码虽然长,我们一步步分析,关键的逻辑就是通过当前的workInProgress.tag去进入到不同的分支,去创建 tag 对应的节点类型。

image.png 其中 workInProgress长这个样子

image.png

第一次传入的 workInProgress是 HostFiber。它的 tag=3。所以我们需要进入这个分支。

        case HostRoot:
          return updateHostRoot(current, workInProgress, renderLanes);

这里有一个关键的地方是他会拿到项目中指定的入口组件(比如 App 组件),并当成下一个 child 绑定到当前 WorkInProgress 上。

var nextProps = workInProgress.pendingProps;
      var prevState = workInProgress.memoizedState;
      var prevChildren = prevState.element;
      cloneUpdateQueue(current, workInProgress);
      processUpdateQueue(workInProgress, nextProps, null, renderLanes);
      var nextState = workInProgress.memoizedState;
      var root = workInProgress.stateNode;
      // being called "element".

      var nextChildren = nextState.element;

如果不是 HostRoot 节点的话,他会根据 tag 进入不同的分支,其中一个重要的分支是我们的函数式组件,如果判断当前 tag 是函数式组件的Fiber 的话,会先通过renderWithHooks运行一遍我们的函数,然后得到一个 虚拟dom 对象。 注意,虚拟 dom 对象和 Fiber 节点是会有一层转换关系的,后续的流程体现了这层转换关系。

image.png 然后会通过reconcileChildren函数将传入的虚拟 dom 创建成Fiber 节点。 也就是说他会先运行我们的一遍函数,拿到虚拟 DOM,再通过方法将虚拟 dom 变成 Fiber,最后跟我们的 WorkInProgress 连接。

      function reconcileChildFibers(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes
      ) {
        // This function is not recursive.
        // If the top level item is an array, we treat it as a set of children,
        // not as a fragment. Nested arrays on the other hand will be treated as
        // fragment nodes. Recursion happens at the normal flow.
        // Handle top level unkeyed fragments as if they were arrays.
        // This leads to an ambiguity between <>{[...]}</> and <>...</>.
        // We treat the ambiguous cases above the same.
        var isUnkeyedTopLevelFragment =
          typeof newChild === "object" &&
          newChild !== null &&
          newChild.type === REACT_FRAGMENT_TYPE &&
          newChild.key === null;

        if (isUnkeyedTopLevelFragment) {
          newChild = newChild.props.children;
        } // Handle object types

        if (typeof newChild === "object" && newChild !== null) {
          switch (newChild.$$typeof) {
            case REACT_ELEMENT_TYPE:
              return placeSingleChild(
                reconcileSingleElement(
                  returnFiber,
                  currentFirstChild,
                  newChild,
                  lanes
                )
              );

            case REACT_PORTAL_TYPE:
              return placeSingleChild(
                reconcileSinglePortal(
                  returnFiber,
                  currentFirstChild,
                  newChild,
                  lanes
                )
              );

            case REACT_LAZY_TYPE: {
              var payload = newChild._payload;
              var init = newChild._init; // TODO: This function is supposed to be non-recursive.

              return reconcileChildFibers(
                returnFiber,
                currentFirstChild,
                init(payload),
                lanes
              );
            }
          }

          if (isArray(newChild)) {
            return reconcileChildrenArray(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes
            );
          }

          if (getIteratorFn(newChild)) {
            return reconcileChildrenIterator(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes
            );
          }

          throwOnInvalidObjectType(returnFiber, newChild);
        }

        if (typeof newChild === "string" || typeof newChild === "number") {
          return placeSingleChild(
            reconcileSingleTextNode(
              returnFiber,
              currentFirstChild,
              "" + newChild,
              lanes
            )
          );
        }

        {
          if (typeof newChild === "function") {
            warnOnFunctionType(returnFiber);
          }
        } // Remaining cases are all treated as empty.

        return deleteRemainingChildren(returnFiber, currentFirstChild);
      }

      return reconcileChildFibers;
    }

如果当前 beginWork 返回 null 的时候,说明我们走完了 beginWork 阶段,从这里开始就进入了 completeUnitOfWork(unitOfWork)阶段。该阶段主要有以下几个作用吧:

  1. 创建真实的 dom 元素
  2. 将真实的 dom 元素插入置顶指定的位置
  3. 初始化 Fiber 相关属性
  4. flags 冒泡

首先看一下如何通过 Fiber 创建真实的 dom 节点并赋值给 stateNode。 开始之前明白一个点,不是所有的 fiber 节点都会创建真实的 dom 元素,我们以 tag 为 5 的 HostComponent 为例:

    case HostComponent:
     var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
            appendAllChildren(instance, workInProgress, false, false);
            workInProgress.stateNode = instance; // Certain renderers require commit-time effects for initial mount.
            // (eg DOM renderer supports auto-focus for certain elements).
            // Make sure such renderers get scheduled for later work.

            if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
              markUpdate(workInProgress);
            }
            bubbleProperties()

主要就是通过createInstance 创建了一个instance 实例,然后通过 appendAllChildren 将 instance 插入到指定的位置,最后进行bubble 冒泡的整个流程。

先来看一下createInstance具体做了什么

/ ...
 var domElement = createElement(type, props, rootContainerInstance, parentNamespace);
 return domElement;

其实就是根据当前 fiber 节点的 type 创建了一个真实的 dom 节点。

image.png 下面分析一下appendAllChildren。

  appendAllChildren = function (parent, workInProgress, needsVisibilityToggle, isHidden) {
    // We only have the top Fiber that was created but we need recurse down its
    // children to find all the terminal nodes.
    var node = workInProgress.child;

    while (node !== null) {
      if (node.tag === HostComponent || node.tag === HostText) {
        appendInitialChild(parent, node.stateNode);
      } else if (node.tag === HostPortal) ; else if (node.child !== null) {
        node.child.return = node;
        node = node.child;
        continue;
      }

      if (node === workInProgress) {
        return;
      }

      while (node.sibling === null) {
        if (node.return === null || node.return === workInProgress) {
          return;
        }

        node = node.return;
      }

      node.sibling.return = node.return;
      node = node.sibling;
    }
  };

其实比较简单,总共有以下几步进行处理吧

  1. 拿到下一层的 child,判断 child 是否为HostComponent还是 HostText。如果是这两种,直接插入到 parent 的末尾。
  2. 对兄弟 fiber 执行上述判断过程。
  3. 如果兄弟 fiber 执行完毕或者没兄弟 fiber,对兄弟 fiber 的父 fiber 执行。 注意这个父 fiber 不可能执行到 workInProgress。
  4. 当遍历完毕之后终止。
  5. 当这个方法运行完毕之后,属于当前 fiber 的所有 dom 节点都会按照指定的位置插入到 dom 中。

最后会将生成的instance 真实 dom 节点赋值给 stateNode。 最后一步,会进行一个副作用的冒泡,具体逻辑在bubbleProperties中,我们一起来看一下。

while (child !== null) {
        newChildLanes = mergeLanes(newChildLanes, mergeLanes(child.lanes, child.childLanes));
        subtreeFlags |= child.subtreeFlags;
        subtreeFlags |= child.flags; // When a fiber is cloned, its actualDuration is reset to 0. This value will
        // only be updated if work is done on the fiber (i.e. it doesn't bailout).
        // When work is done, it should bubble to the parent's actualDuration. If
        // the fiber has not been cloned though, (meaning no work was done), then
        // this value will reflect the amount of time spent working on a previous
        // render. In that case it should not bubble. We determine whether it was
        // cloned by comparing the child pointer.

        actualDuration += child.actualDuration;
        treeBaseDuration += child.treeBaseDuration;
        child = child.sibling;
      }

可以看到,就是一个循环,会通过位运算去收集flags。 具体是subtreeFlags |= child.subtreeFlags; 先去收集 child 下面的子孙的 flags

subtreeFlags |= child.flags; 再去收集 child 标记的 flags。 整个执行完毕之后,副作用会向上冒泡一层。

试想一下,如果整个过程中执行完毕,当我们需要确定当前 fiber 的子孙 fiber 的副作用的时候,直接可以通过subtreeFlags 来快速确定"该 fiberNode 所在子树是否存在副作用需要执行。"

当整个 beginWork 和completeWork都执行完毕之后, 整个 WorkInProgress 树也被构造出来了,下面我们来看一下构造出来的树长什么样子。

image.png