React

React 源码(一) 整体流程

By John Han
Picture of the author
Published on
image alt attribute

React 源码(一) 整体流程

说明

  • 源码系列文章基于 React 17.0.1
  • 为了阅读体验,源码会截取一小部分
  • 这里只是粗略地读了一遍,可能有很多细节每来得及理清楚,后面会不断补充

前置知识

1. 我是如何看源码的

无论你读或不读,React 源码都静静地躺在在那里,不好不坏。

需要说明的是,React 源码使用 TypeScript 写的且量很大,直接读起来估计那味道会很酸爽 😂。

因为是 TypeScript 写的源码,所以,在浏览器中调试时看到的都是解析打包的代码,要清楚这一点。

如果把大致的流程清楚之后,还是建议直接读一下 React 没有任何打包的源码,主要原因有三点:

    1. React 源码中的文件和命名对理解 React 是很有帮助的,但是,打包后这些命名都不存在了
    1. React 源码不仅是世界上最优秀的一批前端工程师的杰作,而且其中所涉猎的软件知识范围绝不止前端范畴,打包后的代码可能会有所偏离
    1. 学习 React 源码不仅对我们自己开发基础组件也会有很大帮助,也可以学习 TypeScript

建议直接创建一个专门用来学习源码的小项目:

npx create-react-app react-code
cd react-code
npm start

此时,你的项目中已经有了最新的 React 和相关的 package。


如果你嫌每次到 node_modules 中查找文件太麻烦,可以使用 yarn 的 link 功能,将相关的包指向一个单独的 React 源码文件。

  1. 拉取源码
git clone https://github.com/facebook/react.git
  1. 安装依赖
# 进入到垃取的react项目文件夹
cd react
npm install
  1. build 成 dev 环境使用的 cjs 包
yarn build react/index,react/jsx,react-dom/index,scheduler --type=NODE
  1. 改变项目中依赖包的目录指向
cd build/node_modules/react
# 申明react指向
yarn link
cd build/node_modules/react-dom
# 申明react-dom指向
yarn link
  1. 改变项目包的指向
# 进入我们上面创建的项目
cd react-code
# 将项目内的react react-dom指向之前申明的包
yarn link react react-dom

这样我们的 react-code 项目中使用的就是我们拉下来的 react 源码中的包了。

2. 启动项目

如果已经创建的项目,并安装了依赖,可以快速启动我们的项目:

npm start

3. 一张必看的 React 渲染流程图

F12 打开浏览器的开发者工具(DevTools);

找到 Performance 面板,点击刷新。

performance
performance

你会看到一张页面渲染的火焰图,找到有用的部分,"./src/index.js"所在的部分,其他的先不管。

图 2: 初次渲染,React 执行过程火焰图 🔥

hello function flow
hello function flow

对,这里的 render 就是我们代码里的 ReactDOM.render() 方法。

注意 ⚠️

这是使用 ReactDOM.render()方法时的流程,也就是默认的 legacy 模式,如果用的时 concurrent 模式就不是这样了。

如, concurrent 模式下 render 阶段的入口函数是 performConcurrentWorkOnRoot,而不是 performSyncWorkOnRoot。这一点可以在这里看到。

4. 首次渲染的大致过程

4.1 执行入口调度

4.1.1 执行 render()入口

一般 Web 应用的入口函数是:

ReactDOM.render(<App />, document.getElementById("root"));

在 render 函数处打断点:

render gif
render gif

可以看到,render 的参数:

  • element:处理后得到的 React Element(处理过程在 【深入理解 JSX】章节有介绍)
  • container: 就是我们的容器元素 div#root

4.1.2 执行 legacyRenderSubtreeIntoContainer()

从名称可以看出,这里是要开始渲染子树到容器中。

主要功能:

    1. 初始化 fiberRoot 和 FiberNode
    1. fiberRoot.current 指向了 child 为 null 的 RootFiber(组件树的根 Fiber)
function legacyRenderSubtreeIntoContainer(
  parentComponent,
  children,
  container,
  forceHydrate,
  callback
) {
  ...
  var root = container._reactRootContainer;
  var fiberRoot;

  // 初始化时 root === undefined,会进入判断
  if (!root) {
    // Initial mount

    // root 是根节点 div#root
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate
    );
    // 给 fiberRoot 赋值,fiberRoot是全局唯一的 FiberRootNode 🌟
    // ⚠️:此时 fiberRoot.current === FiberNode {tag: 3,...}
    // fiberRoot.current 指向了一个空的 RootFiber(执行一次 ReactDOM.render 会生成一个 RootFiber)
    fiberRoot = root._internalRoot;
    ...

    unbatchedUpdates(function () {
      /* 执行更新容器
       * children:APP组件的 JSX解析值
       * fiberRoot: 全局唯一 FiberRootNode
       */
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else { // 这里处理更新逻辑
    fiberRoot = root._internalRoot;

    // Update
    updateContainer(children, fiberRoot, parentComponent, callback);
  }

  return getPublicRootInstance(fiberRoot);
}

在 legacyRenderSubtreeIntoContainer 函数处打断点:

render legacyRenderSubtreeIntoContainer
render legacyRenderSubtreeIntoContainer

4.1.3 执行 updateContainer 函数

主要功能:

  • 创建 update 对象
  • 将 update 添加到队列 ?
  • 开启调度
function updateContainer(element, container, parentComponent, callback) {
  ...
  // current$1 赋值为 RootFiber(组件树的根 Fiber)
  var current$1 = container.current;
  ...
  // 创建 update对象
  var update = createUpdate(eventTime, lane);
  update.payload = {
    element: element,
  };
  callback = callback === undefined ? null : callback;
  ...
  // 将 update 添加到队列 ?
  enqueueUpdate(current$1, update);
  // 开启调度
  var root = scheduleUpdateOnFiber(current$1, lane, eventTime);

  if (root !== null) {
    entangleTransitions(root, current$1, lane);
  }

  return lane;
}

4.1.4 执行 scheduleUpdateOnFiber 函数

主要功能:做更新调度,获取优先级最高的更新

  • if (root === workInProgressRoot): 判断是不是要更新一个正在更新的 fiber 树
  • if (lane === SyncLane): 判断是不是做同步更新
// fiber:RootFiber(组件树的根 Fiber)
function scheduleUpdateOnFiber(fiber, lane, eventTime) {
  ...
  // 标记 fiber更新到 root上,并返回 root
  var root = markUpdateLaneFromFiberToRoot(fiber, lane);
  ...
  // Mark that the root has a pending update.
  markRootUpdated(root, lane, eventTime);

  // 处理正在渲染中的 fiber树 的更新
  if (root === workInProgressRoot) {
    ...
  }

  // 同步更新
  if (lane === SyncLane) {
    // 挂载时进入条件
    if ((executionContext & LegacyUnbatchedContext) !== NoContext &&
     (executionContext & (RenderContext | CommitContext)) === NoContext) {
      schedulePendingInteractions(root, lane);

      // 调度器(schedule阶段)工作完成,开启下一步 render
      performSyncWorkOnRoot(root);
    } else { // 更新时进入条件
      ensureRootIsScheduled(root, eventTime);
      schedulePendingInteractions(root, lane);

      if (executionContext === NoContext) {
        // Flush the synchronous work now, unless we're already working or inside
        // a batch
        resetRenderTimer();
        flushSyncCallbackQueue();
      }
    }
  } else {
    // Schedule other updates after in case the callback is sync.
    ensureRootIsScheduled(root, eventTime);
    schedulePendingInteractions(root, lane);
  }

  return root;
}

4.2 协调器(render 阶段)

4.2.1 执行 performSyncWorkOnRoot 函数

  • renderRootSync(): 启动 render 阶段,获取需要渲染的 Fiber 树
  • commitRoot():启动 commit 阶段, 将获取的 Fiber 树渲染到页面

renderRootSynccommitRoot 是同步执行的,先执行renderRootSync,协调器工作(render 阶段)完成后得到要更新的结果,然后commitRoot做渲染(commit 阶段)工作。

function performSyncWorkOnRoot(root) {
  if (!((executionContext & (RenderContext | CommitContext)) === NoContext)) {
    {
      throw Error("Should not already be working.");
    }
  }

  flushPassiveEffects();
  var lanes;
  var exitStatus;

  // 优化处理,继续渲染一棵未完成的树
  if (
    root === workInProgressRoot &&
    includesSomeLane(root.expiredLanes, workInProgressRootRenderLanes)
  ) {
    lanes = workInProgressRootRenderLanes;
    exitStatus = renderRootSync(root, lanes);
  } else {
    // 正常的 mount 和 update执行这里
    lanes = getNextLanes(root, NoLanes);

    {
      var nextLanesPriority = returnNextLanesPriority();

      if (nextLanesPriority < InputDiscreteLanePriority) {
        return null;
      }
    }
    // 启动协调器(render 阶段)
    exitStatus = renderRootSync(root, lanes);
  }

  // 错误处理
  if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
    ...
    // If something threw an error, try rendering one more time.
  }

  ...
  // We now have a consistent tree. Because this is a sync render, we will commit it even if something suspended.
  // 翻译下官网注释:现在我们得到了一个一致的 Fiber树. 因为渲染是同步进行的,就算有要暂停地方,我们仍要 commit 它。
  var finishedWork = root.current.alternate;
  root.finishedWork = finishedWork;
  root.finishedLanes = lanes;
  // 启动渲染器(commit阶段)
  commitRoot(root);

  ensureRootIsScheduled(root, now());
  return null;
}

4.2.2 执行 renderRootSync 函数

启动协调器(render 阶段)。

function renderRootSync(root, lanes) {
  ...
  /*
   * workInProgressRoot: 我们正在 working 的 root
   * workInProgressRootRenderLanes: 正在 rending 的 lanes
   */
  if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    /*
     * 准备好要更新的栈 -- 新的 Fiber 树
     * 🌟 会调用 createWorkInProgress(root.current, null) 🌟 创建 workInProgress(The fiber we're working on)
     */
    prepareFreshStack(root, lanes);
    startWorkOnPendingInteractions(root, lanes);
  }
  ...
  do {
    try {
      workLoopSync();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);
  ...
}

有必要看下 prepareFreshStack():

function prepareFreshStack(root, lanes) {
  ...
  // 初始化 workInProgressRoot
  workInProgressRoot = root;
  // 创建当前 Fiber节点,然后赋值给变量 workInProgress 🌟
  workInProgress = createWorkInProgress(root.current, null);
  ...
}

遍历执行 “递” 操作:

function workLoopSync() {
  // workInProgress 不为 null 时,循环♻️ 调用 performUnitOfWork
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

4.2.3 执行 performUnitOfWork 函数

会创建下一个 Fiber 节点并赋值给 workInProgress.

这里执行 Fiber 树的深度优先遍历工作

如果没有下一个需要“递”的节点,会终止上面 👆 的 循环 ♻️。

// unitOfWork 是当前 workInProgress 节点
function performUnitOfWork(unitOfWork) {
  // alternate 是 双缓存Fiber 中对应的另一个 Fiber树 中相应的 Fiber节点
  // 只有初始化时 current 为 null,其他情况下 current指向上一次更新时对应的 Fiber节点 🌟
  var current = unitOfWork.alternate;
  setCurrentFiber(unitOfWork);
  var next;

  if ((unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    // 调用“递”工作 🌟🌟
    // next 代表下一个需要“递”的 Fiber节点
    next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
  }

  // 重置 current = null等
  resetCurrentFiber();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;

  // next 为 null 时开始“归”操作
  if (next === null) {
    // 调用“归”工作 🌟🌟
    completeUnitOfWork(unitOfWork);
  } else {
    // next 不为 null 时,将 next 赋值为新的 workInProgress,继续循环♻️
    workInProgress = next;
  }

  ReactCurrentOwner$2.current = null;
}

4.2.4 执行 beginWork$1 函数

  • 执行 beginWork 函数
  • 如果出错就恢复为 beginWork 之前的 Fiber
beginWork$1 = function (current, unitOfWork, lanes) {
  // 在进入 begin 阶段之前,将正在进行的工作复制到一个 dummyFiber 上
  var originalWorkInProgressCopy = assignFiberPropertiesInDEV(dummyFiber, unitOfWork);

  try {
    // 启动“递”阶段
    return beginWork(current, unitOfWork, lanes);
  } catch (originalError) {
    ...
    // 如果 beginWork 抛出,我们就用这个来重置状态
    assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy);

    ...
    throw originalError;
  }
};

4.2.5 执行 beginWork 函数

就是要根据当前 Fiber 节点,创建子 Fiber 节点

beginWork 每次只会创建一个 子 Fiber 节点。

beginWork 做了大量的工,源码地址,源码中整个文件夹都是 beginWork 相关的函数。

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // 组件更新优先级
  let updateLanes = workInProgress.lanes;

  // 处理开发环境相关工作
  if (__DEV__) {...}

  // 处理 update(更新)阶段工作
  if (current !== null) {
    var oldProps = current.memoizedProps;
    var newProps = workInProgress.pendingProps;

    // 如果 props 或 context 发生了变化,则标明该 fiber 已进行工作
    if (oldProps !== newProps || hasContextChanged() || ( // Force a re-render if the implementation changed due to hot reload:
     workInProgress.type !== current.type )) {

      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) { // 当前Fiber节点优先级不够
      didReceiveUpdate = false;
      // 这根 Fiber 没有任何待处理的工作
      // 不再继续 beginWork,但是要是把东西推到堆栈上
      switch (workInProgress.tag) {...};

      // 复用 current
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  } else {
    // 可以直接复用前一次更新的子Fiber,不需要新建子Fiber
    didReceiveUpdate = false;
  }

  // 进入begin阶段前,清除待更新优先级
  workInProgress.lanes = NoLanes;

  // 匹配不同的 FiberNode,调用不同的方法做进一步处理
  switch (workInProgress.tag) {
    ...
    case FunctionComponent: {}
    case ClassComponent: {}
    case HostRoot: {}
    case HostComponent: {}
    case HostText: {}
    case SuspenseComponent: {}
    ...
  }
}

beginWork() 参数解析:

  • current: 当前要做“递”操作的 FiberNode 在上一次更新时对应的 FiberNode,即 workInProgress.alternate
  • workInProgress:当前要做“递”操作的 FiberNode
  • renderLanes:优先级相关

组件 mount 时: current === null; update 时: current !== null;

所以,beginWork 会用 if (current !== null) 判断来区分 mountupdate.

进入 update 时: 会判断对 workInProgress 的创建进行优化。

进入 mount 时: 会根据不同的 tag 类型,创建不同的 Fiber 节点。

无论哪一种类型,都会进入 reconcileChildren 方法。

示例:

function App() {
  return (
    <div className="App">
      <FuncCom></FuncCom>
      <ClassCom></ClassCom>
    </div>
  );
}

FuncCom 组件:

return (
  <div class="funcCom">
    <button onClick={() => click}>func click</button>
  </div>
);

div.APP 的 type = "div",tag: 5, 属于 HostComponent;

FuncCom 组件是个函数组件,tag: 2, type = () => {name: FUNC, length: 0 ...}

”递“ FuncCom 组件的 div,

”递“ FuncCom 组件的 button, type: "button", tag: 5, 属于 HostComponent;

由于 button 已经没有子节点(文本节点“func click”做了优化,不会去遍历),所以“深度优先”的遍历到了一个尾端,需要向上“归”了。

“归”操作调用 completeWork()

button 的 FiberNode.return 指向 div.funcCom。

由于 div.funcCom 没有其他兄弟节点,需要接续网上”归“,div.funcCom 的 FiberNode.return 指向 FuncCom 的 FiberNode。

下一步会查找 FuncCom 的兄弟节点,找到了 ClassCom。

FuncCom 组件是个类组件,tag: 2, type: class ClassCom {name: "ClassCom", length: 0 ...}

然后继续这样的递归操作,直到“归”到这 fiber 根节点(fiberRoot), tag: 3, type: null。

这时,就完成了 render 阶段的工作,协调器的工作做完了。

以上任务全部是发生在内存中的,因此,用户不会有任何感知

调用栈会返回到 performSyncWorkOnRoot 函数,执行 commitRoot(root)。

会进入下一个阶段(commit 阶段),渲染器开始做渲染工作

4.2.7 执行 reconcileChildren

reconcileChildren 会调用 reconcileChildFibers,这是协调器(render 阶段)最重要的方法。

mount 时: 它会创建所有的 子 Fiber 节点.

update 时: 它会将 ”当前节点“ 与该节点在 "上次更新时对应的 Fiber 节点"newChild(React Element) 做比较(这个比较就是 diff 算法),最终生成新的需要渲染的 Fiber 节点。🌟🌟🌟

newChild(React Element) 做比较之前最终会调用 createFiber()方法先生成 Fiber 节点。

createFiber 方法生成 fiber 节点:

const createFiber = function (
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode
): Fiber {
  // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
  return new FiberNode(tag, pendingProps, key, mode);
};

reconcileChildren 方法:

reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  // 如果这是一个尚未渲染的全新组件
  // 不会通过应用最小的副作用来更新其子集,会在渲染之前将它们全部添加到子集中
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 如果当 current.child 与 workInProgress 相同,说明我们还没有开始对这些 children 进行任何工作,
    // 我们使用 克隆算法 来创建所有 current.child 的副本

    // 如果有不同的变动,就挑出来
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

mountChildFibers()reconcileChildFibers()这两个方法的源码可以看到,它们都调用了 ChildReconciler 方法。

区别是 reconcileChildFibers()方法生成的 Fiber 节点有 effectTag 属性,commit 阶段渲染时,执行 DOM 操作的具体类型就保存在 fiber.effectTag 中。🌟

ReactChildFiber.old.js 里的源码:

...
//省略各种处理具体逻辑
...
// 这是个包裹函数,返回一个新的函数,新的函数
function ChildReconciler(shouldTrackSideEffects) {
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes
  ): Fiber | null {
    // 处理 <>{[...]}</> and <>...</> 的片段
    const isUnkeyedTopLevelFragment =
      typeof newChild === "object" &&
      newChild !== null &&
      newChild.type === REACT_FRAGMENT_TYPE &&
      newChild.key === null;
    if (isUnkeyedTopLevelFragment) {
      newChild = newChild.props.children;
    }

    // 处理 object 类型
    const isObject = typeof newChild === "object" && newChild !== null;

    if (isObject) {
      ...
    }

    // 处理 string 和 number类型
    if (typeof newChild === "string" || typeof newChild === "number") {
      ...
    }

    // 处理数组
    if (isArray(newChild)) {
      ...
    }

    // 处理 迭代器函数
    if (getIteratorFn(newChild)) {
      ...
    }

    ...
    // 处理异常情况
    if (typeof newChild === "undefined" && !isUnkeyedTopLevelFragment) {
      ...
    }

    // 其他情况按清空处理
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }

  return reconcileChildFibers;
}
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

深度优先的递归遍历节点,“递”的工作会在二叉树的终点开始“归”操作,此时说明当前节点的 child 为 null.

最终,beginWork$1 会获取到这个结果,next 会被赋值为 null.

4.2.3 中的 performUnitOfWork 方法会执行 completeUnitOfWork():

// next 为 null 时开始“归”操作
if (next === null) {
  // 调用“归”工作 🌟🌟
  completeUnitOfWork(unitOfWork);
} else {
  // next 不为 null 时,将 next 赋值为新的 workInProgress,继续循环♻️
  workInProgress = next;
}

beginWork 流程图:

(来自-卡颂) 这张图画的挺好,红色为我的标注,后面有时间自己画一个更详细点的

beginWork 流程图
beginWork 流程图

4.2.6 执行 completeWork 函数

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      bubbleProperties(workInProgress);
      return null;
    case ClassComponent: {
      const Component = workInProgress.type;
      if (isLegacyContextProvider(Component)) {
        popLegacyContext(workInProgress);
      }
      bubbleProperties(workInProgress);
      return null;
    }
    case HostRoot: {
      const fiberRoot = (workInProgress.stateNode: FiberRoot);
      ...
      updateHostContainer(current, workInProgress);
      bubbleProperties(workInProgress);
      return null;
    }
    case HostComponent: {
      popHostContext(workInProgress);
      const rootContainerInstance = getRootHostContainer();
      const type = workInProgress.type;
      // update时
      if (current !== null && workInProgress.stateNode != null) {
        // 更新组件
        updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance,
        );
        ...
      } else { // mount时
        ....
        const currentHostContext = getHostContext();
        // 为fiber创建对应DOM节点
        const instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
        // 将子孙DOM节点插入刚生成的DOM节点中
        appendAllChildren(instance, workInProgress, false, false);
        // DOM节点赋值给fiber.stateNode
        workInProgress.stateNode = instance;

        // 与update逻辑中的updateHostComponent类似的处理props的过程
        if (
          finalizeInitialChildren(
            instance,
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
          )
        ) {
          markUpdate(workInProgress);
        }
      }
    }
    ...
    case CacheComponent: {
      if (enableCache) {
        const cache: Cache = workInProgress.memoizedState.cache;
        popCacheProvider(workInProgress, cache);
        bubbleProperties(workInProgress);
        return null;
      }
    }
  }
  ...
}

HostComponent 是处理 tag: 5 (原生 DOM 组件生成的 Fiber 节点类型)时执行的函数。

update 时:


当 update 时,Fiber 节点已经存在对应 DOM 节点,所以不需要生成 DOM 节点。需要做的主要是处理 props,比如:

  • onClick、onChange 等回调函数的注册
  • 处理 style prop
  • 处理 DANGEROUSLY_SET_INNER_HTML prop
  • 处理 children prop

updateHostComponent 中,被处理完的 props 会被赋值给 workInProgress.updateQueue,并最终会在 commit 阶段被渲染在页面上

mount 时:


由于 completeWork 属于“归”阶段调用的函数,每次调用 appendAllChildren 时都会将已生成的子孙 DOM 节点插入当前生成的 DOM 节点下。

那么当“归”到 rootFiber 时,我们已经有一个构建好的离屏 DOM 树

completeWork 流程图:

(来自-卡颂) 这张图画的挺好,红色为我的标注,后面有时间自己画一个更详细点的

completeWork 流程图
completeWork 流程图

4.3 渲染器(commit 阶段)

**渲染器(commit 阶段)**的主要工作是将 Fiber 树(虚拟 DOM)渲染到页面。

渲染器的工作可以分为三个阶段:

  • before mutation 阶段(执行 DOM 操作前)
  • mutation 阶段(执行 DOM 操作)
  • layout 阶段(执行 DOM 操作后)

这三个阶段之外还做了一些额外工作,如 useEffect 触发、优先级重置、ref 绑定/解绑等。

【4.2.1 章节中的 performSyncWorkOnRoot 函数】里完成 render 阶段的工作(执行 renderRootSync)后调用 commitRoot 方法,

最终它会调用 commitRootImpl 方法。

启动 commit 阶段。

源码中的 commitRootImpl 方法

// root:fiberRootNode
// renderPriorityLevel: 优先级
function commitRootImpl(root, renderPriorityLevel) {
  // ------------------------ before mutation 之前
  do {
    // `flushPassiveEffects`会在最后调用`flushSyncUpdateQueue`,
    // 这意味着`flushPassiveEffects`有时会导致额外的被动效果,如触发useEffect回调与其他同步任务
    // 所以我们需要一直循环刷新,直到没有更多待处理的效果
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);
  flushRenderPhaseStrictModeWarningsInDEV();

  ...
  // root.finishedWork 指当前应用的 rootFiber 🌟
  const finishedWork = root.finishedWork;
  const lanes = root.finishedLanes;

  ...
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  ...

  // 重置Scheduler绑定的回调函数
  root.callbackNode = null;
  root.callbackPriority = NoLanePriority;

  let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
  // 重置优先级相关变量
  markRootFinished(root, remainingLanes);

  // 重置全局变量
  if (root === workInProgressRoot) {
    // We can reset these now that they are finished.
    workInProgressRoot = null;
    workInProgress = null;
    workInProgressRootRenderLanes = NoLanes;
  } else {
  }

  // If there are pending passive effects, schedule a callback to process them.
  // 如果有待处理的被动效果,安排回调处理.
  // Do this as early as possible, so it is queued before anything else that might get scheduled in the commit phase.
  // 尽可能早地这样做,这样它就会插入 commit阶段 可能被安排的其他事情的 队列之前
  // 调度 useEffect 🪝🪝
  if (
    (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
    (finishedWork.flags & PassiveMask) !== NoFlags
  ) {
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      scheduleCallback(NormalSchedulerPriority, () => {
        flushPassiveEffects();
        return null;
      });
    }
  }

  // Check if there are any effects in the whole tree.
  // 检查整个树中是否有任何 effects
  const subtreeHasEffects =
    (finishedWork.subtreeFlags &
      (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
    NoFlags;
  const rootHasEffect =
    (finishedWork.flags &
      (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
    NoFlags;

  // 处理有 effects 的情况
  if (subtreeHasEffects || rootHasEffect) {
    ...

    // Reset this to null before calling lifecycles
    // 在调用 lifecycles 之前,将其重置为 null
    ReactCurrentOwner.current = null;

    /*
     * 官方注释:
     * commit阶段 分为几个子阶段
     * 我们对每个阶段的 effect list 进行单独的传递:所有的 mutation effects 在所有 layout effects 之前,以此类推
     */

    // ------------------------ before mutation 阶段----------------------------------


    // 我们在 mutation 之前使用这个阶段来读取 host tree 的状态
    // 调用 getSnapshotBeforeUpdate
    const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
      root,
      finishedWork
    );

    ...

    // ------------------------ mutation 阶段----------------------------------

    // mutate host tree.
    // 页面视图更新在这个阶段 🌟🌟🌟
    commitMutationEffects(root, renderPriorityLevel, finishedWork);

    if (shouldFireAfterActiveInstanceBlur) {
      afterActiveInstanceBlur();
    }
    resetAfterCommit(root.containerInfo);

    // The work-in-progress tree is now the current tree. This must come after
    // the mutation phase, so that the previous tree is still current during
    // componentWillUnmount, but before the layout phase, so that the finished
    // work is current during componentDidMount/Update.
    root.current = finishedWork;

    // ------------------------ layout 阶段----------------------------------

    // 调用副作用(effects) 在 host tree mutation之后读取它
    // 这阶段主要用来完成 layout(布局)
    // 但由于传统原因,类组件的 🌟生命周期也会在这里启动🌟
    ...
    commitLayoutEffects(finishedWork, root, lanes);
    ...

    // Tell Scheduler to yield at the end of the frame, so the browser has an opportunity to paint.
    // 告诉 Scheduler 在帧结束时让步,这样浏览器就有机会绘制
    requestPaint();

    if (enableSchedulerTracing) {
      popInteractions(((prevInteractions: any): Set<Interaction>));
    }
    executionContext = prevExecutionContext;

    if (previousLanePriority != null) {
      // Reset the priority to the previous non-sync value.
      setCurrentUpdateLanePriority(previousLanePriority);
    }
  } else {
    // No effects.
    // 没有副作用直接切换 current 指向 finishedWork
    root.current = finishedWork;
    ...
  }

  // ------------------------ layout 阶段之后 做的其他工作

  const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;

  // useEffect相关
  if (rootDoesHavePassiveEffects) {
    // This commit has passive effects. Stash a reference to them. But don't
    // schedule a callback until after flushing layout work.
    rootDoesHavePassiveEffects = false;
    rootWithPendingPassiveEffects = root;
    pendingPassiveEffectsLanes = lanes;
    pendingPassiveEffectsRenderPriority =
      renderPriorityLevel === NoLanePriority
        ? DefaultLanePriority
        : renderPriorityLevel;
  }

  // Read this again, since an effect might have updated it
  remainingLanes = root.pendingLanes;

  // 性能优化相关
  if (remainingLanes !== NoLanes) {
    ...
  } else {
    legacyErrorBoundariesThatAlreadyFailed = null;
  }

  // 性能优化相关
  if (enableSchedulerTracing) {
    if (!rootDidHavePassiveEffects) {
      finishPendingInteractions(root, lanes);
    }
  }

  // 检测无限循环的同步任务
  if (includesSomeLane(remainingLanes, (SyncLane: Lane))) {
    ...
  } else {
    nestedUpdateCount = 0;
  }

  ...

  // Always call this before exiting `commitRoot`, to ensure that any additional work on this root is scheduled.
  // 总是在退出' commitRoot '之前调用这个函数,确保任何附加的任务被调度
  ensureRootIsScheduled(root, now());

  if (hasUncaughtError) {
    hasUncaughtError = false;
    const error = firstUncaughtError;
    firstUncaughtError = null;
    throw error;
  }

  if ((executionContext & LegacyUnbatchedContext) !== NoContext) {
    ...
    return null;
  }

  // If layout work was scheduled, flush it now. -> 如果布局工作已经安排好了,现在就进行冲洗

  // 执行同步任务,这样同步任务不需要等到下次事件循环再执行
  // 比如在 componentDidMount 中执行 setState 创建的更新会在这里被同步执行
  // 或useLayoutEffect
  flushSyncCallbackQueue();

  ...

  return null;
}

从上面可以看出:

before mutation 之前:

    1. 主要做一些变量赋值,状态重置的工作
    1. 调度 useEffect

4.3.1 before mutation 阶段

主要功能:

    1. 处理 DOM 节点渲染/删除后的 autoFocus、blur 逻辑
    1. 调用 getSnapshotBeforeUpdate 生命周期钩子

before mutation 阶段commitBeforeMutationEffects开始。

commitBeforeMutationEffects 方法:

export function commitBeforeMutationEffects(
  root: FiberRoot,
  firstChild: Fiber
) {
  // focus blur相关
  focusedInstanceHandle = prepareForCommit(root.containerInfo);

  nextEffect = firstChild;
  // 调用主函数
  commitBeforeMutationEffects_begin();

  // We no longer need to track the active instance fiber
  const shouldFire = shouldFireAfterActiveInstanceBlur;
  shouldFireAfterActiveInstanceBlur = false;
  focusedInstanceHandle = null;

  return shouldFire;
}

同步调用如下函数: commitBeforeMutationEffects_begin -> commitBeforeMutationEffects_complete -> commitBeforeMutationEffectsOnFiber:主函数

function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;

  ...

  if ((flags & Snapshot) !== NoFlags) {
    // 处理不同的 tag, 单独处理 ClassComponent 和 HostRoot
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
        break;
      }
      case ClassComponent: {
        if (current !== null) {
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;
          ...
          // 调用 🪝 getSnapshotBeforeUpdate生命周期钩子 🪝
          const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState
          );
          ...
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
        break;
      }
      case HostRoot: {
        if (supportsMutation) {
          const root = finishedWork.stateNode;
          clearContainer(root.containerInfo);
        }
        break;
      }
      case HostComponent:
      case HostText:
      case HostPortal:
      case IncompleteClassComponent:
        break;
      default: {
        ...
      }
    }

    resetCurrentDebugFiberInDEV();
  }
}

4.3.2 mutation 阶段

mutation 阶段从 commitMutationEffects 方法开始.

最终调用主函数 commitMutationEffectsOnFiber 来处理不同类型(Flag 不同)的操作。

主要函数调用流程:

commitMutationEffects -> commitDeletion/commitMutationEffects_complete -> commitMutationEffectsOnFiber.

处理需要删除的元素:

...
if (deletions !== null) {
  for (var i = 0; i < deletions.length; i++) {
    var childToDelete = deletions[i];

    {
      invokeGuardedCallback(null, commitDeletion, null, root, childToDelete, fiber, renderPriorityLevel);

      if (hasCaughtError()) {
        var error = clearCaughtError();
        captureCommitPhaseError(childToDelete, fiber, error);
      }
    }
  }
}
...

ClassComponent 的删除流程:

commitDeletion -> unmountHostComponents -> commitUnmount -> safelyCallComponentWillUnmount -> callComponentWillUnmountWithTimer

删除时,classComponent 的卸载:

生命周期钩子 🪝 componentWillUnmount 会被调用。

var callComponentWillUnmountWithTimer = function (current, instance) {
  instance.props = current.memoizedProps;
  instance.state = current.memoizedState;

  {
    instance.componentWillUnmount();
  }
};

处理 mutation 的元素:

function commitMutationEffects_complete(
  root: FiberRoot,
  renderPriorityLevel: LanePriority
) {
  // 遍历 nextEffect
  while (nextEffect !== null) {
    const fiber = nextEffect;
    if (__DEV__) {
      ...
    } else {
      try {
        // 调用 mutation 主函数
        commitMutationEffectsOnFiber(fiber, root, renderPriorityLevel);
      } catch (error) {
        captureCommitPhaseError(fiber, fiber.return, error);
      }
    }

    const sibling = fiber.sibling;
    if (sibling !== null) {
      ensureCorrectReturnPointer(sibling, fiber.return);
      nextEffect = sibling;
      return;
    }

    nextEffect = fiber.return;
  }
}
function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  renderPriorityLevel: LanePriority
) {
  const flags = finishedWork.flags;

  // 根据 ContentReset effectTag重置文字节点
  if (flags & ContentReset) {
    commitResetTextContent(finishedWork);
  }

  // 更新ref
  if (flags & Ref) {
    const current = finishedWork.alternate;
    if (current !== null) {
      commitDetachRef(current);
    }
    if (enableScopeAPI) {
      // TODO: This is a temporary solution that allowed us to transition away
      // from React Flare on www.
      if (finishedWork.tag === ScopeComponent) {
        commitAttachRef(finishedWork);
      }
    }
  }

  // 处理不同 flags 的 Fiber节点
  const primaryFlags = flags & (Placement | Update | Hydrating);
  outer: switch (primaryFlags) {
    // 插入 DOM
    case Placement: {
      commitPlacement(finishedWork);
      // Clear the "placement" from effect tag so that we know that this is
      // inserted, before any life-cycles like componentDidMount gets called.
      // 从 effect tag 中清除 "placement",这样我们就可以在任何生命周期(如componentDidMount)被调用之前,知道这个被插入了。
      finishedWork.flags &= ~Placement;
      break;
    }
    // 插入DOM 并 更新
    case PlacementAndUpdate: {
      // Placement
      commitPlacement(finishedWork);
      finishedWork.flags &= ~Placement;

      // Update
      const current = finishedWork.alternate;
      commitWork(current, finishedWork);
      break;
    }
    // SSR
    case Hydrating: {
      finishedWork.flags &= ~Hydrating;
      break;
    }
    // SSR
    case HydratingAndUpdate: {
      finishedWork.flags &= ~Hydrating;

      // Update
      const current = finishedWork.alternate;
      commitWork(current, finishedWork);
      break;
    }
    // 更新
    case Update: {
      const current = finishedWork.alternate;
      commitWork(current, finishedWork);
      break;
    }
  }
}

可以看到,mutation 阶段对每个 Fiber 节点执行如下三个操作:

    1. 重置文字节点(由 flags 类型决定)
    1. 更新 ref
    1. 根据 fiber 节点的 flags 做不同的处理(Placement | Update | Deletion | Hydrating)

Placement Flags:

function commitPlacement(finishedWork: Fiber): void {
  if (!supportsMutation) {
    return;
  }

  // Recursively insert all host nodes into the parent.
  // 递归地将所有主机节点插入到父节点中
  const parentFiber = getHostParentFiber(finishedWork);

  // 注意:parent、isContainer 这两个变量必须一起更新
  let parent;
  let isContainer;
  const parentStateNode = parentFiber.stateNode;
  switch (parentFiber.tag) {
    case HostComponent:
      parent = parentStateNode;
      isContainer = false;
      break;
    case HostRoot:
      parent = parentStateNode.containerInfo;
      isContainer = true;
      break;
    case HostPortal:
      parent = parentStateNode.containerInfo;
      isContainer = true;
      break;
    // eslint-disable-next-line-no-fallthrough
    default:
      invariant(
        false,
        "Invalid host parent fiber. This error is likely caused by a bug " +
          "in React. Please file an issue."
      );
  }
  if (parentFiber.flags & ContentReset) {
    // 在进行任何插入之前,先重置父的文本内容
    resetTextContent(parent);
    // Clear ContentReset from the effect tag
    parentFiber.flags &= ~ContentReset;
  }

  // 获取插入位置之前的兄弟节点
  const before = getHostSibling(finishedWork);
  // 我们只有被插入的顶部Fiber,但我们需要向下递归它的子节点,以找到所有的终端节点
  if (isContainer) {
    // 处理 container(应用 Fiber根节点,FiberNode)
    insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
  } else {
    // 将需要插入的内容
    insertOrAppendPlacementNode(finishedWork, before, parent);
  }
}

从 insertOrAppendPlacementNodeXXX 方法名可以看到,它是和具体的 DOM 操作相关的。

// 插入or添加 PlacementNode 到 container
function insertOrAppendPlacementNodeIntoContainer(node, before, parent) {
  var tag = node.tag;
  var isHost = tag === HostComponent || tag === HostText;

  // Host组件(类HTML,如 div)
  if (isHost) {
    var stateNode = isHost ? node.stateNode : node.stateNode.instance;

    if (before) {
      insertInContainerBefore(parent, stateNode, before);
    } else {
      appendChildToContainer(parent, stateNode);
    }
  } else if (tag === HostPortal);
  else {
    // 处理 复合元素(如 APP,自定义组件)
    var child = node.child;

    if (child !== null) {
      insertOrAppendPlacementNodeIntoContainer(child, before, parent);
      var sibling = child.sibling;

      while (sibling !== null) {
        insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
        sibling = sibling.sibling;
      }
    }
  }
}
// 执行具体的 DOM 操作
function insertOrAppendPlacementNode(
  node: Fiber,
  before: ?Instance,
  parent: Instance
): void {
  const { tag } = node;
  const isHost = tag === HostComponent || tag === HostText;
  // 处理 Host 元素(HTML 元素)
  if (isHost) {
    const stateNode = isHost ? node.stateNode : node.stateNode.instance;
    if (before) {
      insertBefore(parent, stateNode, before);
    } else {
      appendChild(parent, stateNode);
    }
  } else if (tag === HostPortal) {
  } else {
    // 处理 复合元素(自定义组件)
    const child = node.child;
    if (child !== null) {
      // 递归处理 node.child
      insertOrAppendPlacementNode(child, before, parent);
      let sibling = child.sibling;
      while (sibling !== null) {
        insertOrAppendPlacementNode(sibling, before, parent);
        sibling = sibling.sibling;
      }
    }
  }
}

4.3.3 layout 阶段

首先要知道,在 layout 阶段之前已经改变了 root.current 的指向。

root.current = finishedWork;

因为该阶段的代码都是在 DOM 渲染完成(mutation 阶段完成)后执行的。

该阶段触发的生命周期钩子和 hook 可以直接访问到已经改变后的 DOM.

layout 阶段从 commitLayoutEffects() 开始。

函数调用顺序:

commitLayoutEffects -> commitLayoutEffects_begin -> commitLayoutMountEffects_complete -> commitLayoutEffectOnFiber

commitLayoutEffectOnFiber 方法:

会根据 fiber.tag 对不同类型的节点分别处理.

  • 执行了 ClassComponent 的生命周期钩子:

    componentDidMount(mount 时执行) 或 componentDidUpdate(update 时执行)

参考

Stay Tuned

Want to become a Next.js pro?
The best articles, links and news related to web development delivered once a week to your inbox.