最近在学习慕课网 手写 React 高质量源码迈向高阶开发,之前自己也尝试看过源码,不过最终放弃了
放弃的最主要原因是 react 内部的调用链太长了,每天在缕清调用链上都花了不少时间,createRoot 都没有看完
最近看到慕课网有一个 react 源码课,就想着跟着课程然后在自己源码,看看这次能够看到什么地步
它这个课程前八章 是 react@16 的源码,从第九章开始才是 react@18 的源码
从本章开始学习的是 react@18 的源码,从 fiber 开始,涉及 beginWork,completeWork,commitWork,调度策略,优先级等
React 源码系列:
react@16- 第 1 篇:createElement 和 render 函数实现原理
- 第 2 篇:函数组件和类组件及 ref 和 setState 的实现
- 第 3 篇:优化渲染过程之 dom diff
- 第 4 篇:类组件增强------生命周期函数
- 第 5 篇:性能优化------PureComponent 和 memo
- 第 6 篇:hooks------useEffect、useState、useMemo 等源码实现
react@18- 第 1 篇:beginWork 前的准备工作:jsxDEV,createRoot,render 源码实现
- 第 2 篇:beginWork 工作原理
- 第 3 篇:4 张图带你看懂 beginWork 和 completeWork 工作过程
- 第 4 篇:一张图看懂 React 合成事件原理
- 第 5 篇:
commitWork工作原理
通过 beginWork 和 completeWork 两张的讲解,我们已经完成渲染阶段的逻辑,接下来将进入提交阶段 commitWork
先来总结一下,渲染阶段分为:beginWork 和 completeWork:
beginWork的作用是将虚拟DOM生成FibercompleteWork的作用是将Fiber转换成真实DOM,并将子节点挂载到对应的父节点下面
在实际的执行过程中,beginWork 和 completeWork 是交替执行的
具体可以查看这两篇文章:
在渲染阶段完成之后,就将进入提交阶段,也就是 commitWork,commitWork 作用是将真实 DOM 挂载到页面中
获取到已经处理完的 Fiber 树
commitWork 是从根节点开始处理,也就是 FiberRoot,FiberRoot 如下图所示

在得到这个 Fiber 树之前,我们需要做如下处理:
- 从
root.current取出alternate,也就是已经完成处理的Fiber树 - 将
alternate赋值给finishedWork,这样在commitWork阶段就直接使用finishedWork就可以拿到已经处理完成的Fiber树了
代码如下所示:
js
// react-reconciler/src/ReactFiberWorkLoop.js
function performConcurrentWorkOnRoot(root) {
// beginWork 和 completeWork 在此函数中执行
renderRootSync(root);
// 执行到这里时,beginWork 和 completeWork 都已经执行完了
// alternate 是一颗已经完成处理的 Fiber 树
// 需要将 alternate 赋值给 finishedWork,这样在 commitWork 阶段就可以拿到已经完成处理的 Fiber 树了
const finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
commitRoot(root); // commitWork 阶段
}
最终在 commitRoot 开始之前,我们得到的 Fiber 树结构,如下图所示

commitRoot
commitRoot 函数接收 FiberRoot 作为参数
主要的作用是检查 RootFiber 和 RootFiber.child 是否有变化,如果有变化就将 RootFiber 渲染在页面上
如何检查它的变化呢?
如何检查 RootFiber 是否有变化呢?
在 beginWork 阶段,如果创建了新的 Fiber,就会给这个 Fiber 的 flags 属性赋值,如果是新增就是 Placement
在 completeWork 阶段,在处理当前 Fiber 时,会将子 Fiber 的 flags 和 subtreeFlags 赋值给当前 Fiber 的 subtreeFlags
我们就可以直接拿到 finishedWork.flags 和 finishedWork.subtreeFlags,然后和 MutationMask 比较一下就可以知道是否有变化了
根据图中的显示,我们知道 finishedWork.stateNode 是 FiberRoot,FiberRoot 是一个真实节点(document.getElementById("root")),所以它本身不会有变化,也就是 flags 为 0,但是它的 subtreeFlags 是有值的
比较方法是按位与,如果结果不为 0,说明有变化,进入 commitMutationEffectsOnFiber 函数,代码如下所示
js
// react-reconciler/src/ReactFiberWorkLoop.js
function commitRoot(root) {
// 取出已经过 beginWork 和 completeWork 处理过的 Fiber 树
// 这个 finishedWork 是 RootFiber
const { finishedWork } = root;
// 查看 RootFiber 的子 Fiber 是否有处理
/**
* Placement: 0b0000000010;
* MutationMask: 0b0000000110;
* NoFlags: 0b0000000000;
*
* 0b0000000010
* & 0b0000000110
* --------------
* 0b0000000010 !== 0b0000000000
*/
const subtreeHasEffects = (finishedWork.subtreeFlags & MutationMask) != NoFlags;
// 查看 RootFiber 是否有处理
const rootHasEffect = (finishedWork.flags & MutationMask) != NoFlags;
if (subtreeHasEffects || rootHasEffect) {
// 有处理就进入 commitMutationEffectsOnFiber 函数
commitMutationEffectsOnFiber(finishedWork, root);
}
// 经过 commitWork 处理后,将替换页面中的 RootFiber
root.current = finishedWork;
}
flags 和 subtreeFlags 赋值
我们来回顾一下 beginWork 阶段,flags 是怎么赋值的
在 beginWork 阶段,只给 div#root 节点下的第一个子节赋值 flags,代码如下所示
这么做是为了优化性能,因为在初始渲染时,所有的节点都是新增,所以只需要给 div#root 节点下的第一个子节点(element 或者 Component)赋值 flags 就可以了
js
// react-reconciler/src/ReactChildFiber.js
function placeSingleChild(newFiber) {
// beginWork 阶段,只有在处理 div#root 节点时,shouldTrackSideEffects 为 true
// 这里的 newFiber 是 div#root 的第一个子节点,也就是 element 或者 Component
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement;
}
return newFiber;
}
然后在 completeWork 阶段,收集当前 Fiber 的子 Fiber.flags 和 Fiber.subtreeFlags,由于在初始渲染时,只有 div#root 节点下的第一个子节点有 flags 值(这里 React 并没有做优化),所以当遍历到 div#root 节点时,就会拿到 element 或者 Component 的 flags 值,然后赋值给 div#root 节点的 subtreeFlags
js
function bubbleProperties(completedWork) {
let subtreeFlags = NoFlags;
let child = completedWork.child;
while (child !== null) {
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
child = child.sibling;
}
completedWork.subtreeFlags = subtreeFlags;
}
commitMutationEffectsOnFiber
commitWork 就是从这里开始执行的,从 RootFiber 开始,和 recursivelyTraverseMutationEffects 一起递归处理各个节点
commitMutationEffectsOnFiber 函数内部主要调用了两个函数:
recursivelyTraverseMutationEffects:用来递归处理Fiber树commitReconciliationEffects:用来处理自身节点的副作用
recursivelyTraverseMutationEffects 函数从根节点开始往下执行,等到 recursivelyTraverseMutationEffects 执行结束后,commitReconciliationEffects 函数从叶子节点开始往上执行
bash
# recursivelyTraverseMutationEffects 函数处理节点的顺序
div#root -> element/Component
# commitReconciliationEffects 函数处理节点的顺序
element/Component -> div#root
代码如下:
js
// react-reconciler/src/ReactFiberCommitWork.js
function commitMutationEffectsOnFiber(finishedWork, root) {
switch (finishedWork.tag) {
case HostRoot:
case HostComponent:
case HostText: {
// 递归处理 Fiber 树
recursivelyTraverseMutationEffects(finishedWork);
// 处理自身节点的副作用
commitReconciliationEffects(finishedWork);
break;
}
}
}
recursivelyTraverseMutationEffects
recursivelyTraverseMutationEffects 接收一个 Fiber 作为参数
主要功能是检查 Fiber.subtreeFlags 是否有 MutationMask,如果有,则说明子元素有变化,需要递归处理子元素
如图所示:

具体的执行过程如下:
- 函数执行时,首先检查当前
Fiber.subtreeFlags是否有MutationMask- 如果有就调用
commitMutationEffectsOnFiber函数,处理Fiber.child,然后检查这个child是否有兄弟元素,如果有就递归处理它的兄弟元素 - 如果没有变化就结束递归
- 如果有就调用
- 第一次进入时
parentFiber是RootFiber,也就是div#root对应的Fiber - 第二次进入时
parentFiber是RootFiber.child,也就是element或者Component对应的Fiber - 第三次不会进入,因为
RootFiber.child没有sibling,所以递归结束
js
// react-reconciler/src/ReactFiberCommitWork.js
function recursivelyTraverseMutationEffects(parentFiber) {
// 查看子 Fiber 是否需要处理
if (parentFiber.subtreeFlags & MutationMask) {
let { child } = parentFiber;
// 如果有子 Fiber 就循环调用 commitMutationEffectsOnFiber
while (child !== null) {
// 递归处理 Fiber
commitMutationEffectsOnFiber(child);
child = child.sibling;
}
}
}
commitReconciliationEffects
commitReconciliationEffects 函数的作用是检查自身有没有的 flags 有没有 Placement,如果有就调用 commitPlacement 函数处理当前 Fiber 节点,并将 Placement 从 flags 中删除
~Placement 的作用是将 Placement 取反,也就是 0b0000000010 取反,结果是 0b1111111101 然后和 flags 做按位与,将 Placement 从 flags 中删除
js
// react-reconciler/src/ReactFiberCommitWork.js
function commitReconciliationEffects(finishedWork) {
const { flags } = finishedWork;
// 检查自身有没有变化,如果有变化就调用 commitPlacement
if (flags & Placement) {
commitPlacement(finishedWork);
// 处理完后就将 Placement 从 flags 中删除
finishedWork.flags &= ~Placement;
}
}
commitPlacement
commitPlacement 函数的作用是将 Fiber 挂载到页面上
commitPlacement 执行的过程是从当前的 Fiber 开始找到最近的父 Fiber(向上查找,必须是一个真实 DOM 的 Fiber,如果是 ComponentFiber,继续向上查找),然后根据父节点的类型,拿到对应的 DOM 节点(父节点)
在初始渲染阶段,finishedWork 是 RootFiber.child,也就是 element 或者 Component 对应的 Fiber
然后调用 insertOrAppendPlacementNode 函数,将 finishedWork 插入到 parent 中
js
// react-reconciler/src/ReactFiberCommitWork.js
// 在初始渲染阶段,finishedWork 是 RootFiber.child,也就是 element 或者 Component 对应的 Fiber
// parentFiber 是 RootFiber
function commitPlacement(finishedWork) {
const parentFiber = getHostParentFiber(finishedWork);
switch (parentFiber.tag) {
// 初始渲染会走这里
case HostRoot: {
const parent = parentFiber.stateNode.containerInfo;
insertOrAppendPlacementNode(finishedWork, null, parent);
break;
}
}
}
getHostParentFiber
getHostParentFiber 函数的作用是找到当前 Fiber 最近的父 Fiber(这个父 Fiber 必须有真实 DOM 节点的 Fiber,否者继续向上查找)
如何找到父 Fiber
看下 Fiber.return.tag 是不是 HostComponent 或者 HostRoot,如果是说明这个 Fiber 有真实的 DOM 节点,如果不是就继续往上找
js
// react-reconciler/src/ReactFiberCommitWork.js
// 从当前 Fiber 向上找到最近有真实 DOM 节点的 Fiber
function getHostParentFiber(fiber) {
let parent = fiber.return;
while (parent !== null) {
if (isHostParent(parent)) {
return parent;
}
parent = parent.return;
}
}
// react-reconciler/src/ReactFiberCommitWork.js
function isHostParent(fiber) {
return fiber.tag === HostComponent || fiber.tag === HostRoot;
}
insertOrAppendPlacementNode
insertOrAppendPlacementNode 函数的作用是将 Fiber 挂载到 parent 中,接收三个参数:
fiber:需要挂载的Fiber,初始渲染时是element或者Component对应的Fiberbefore:需要挂载Fiber的前一个兄弟节点,初始渲染时为nullparent:fiber的父元素,初始渲染时为div#root
这个函数的主要作用是
- 判断是不是真实的
DOM节点,如果是就从fiber中拿到真实的DOM节点(stateNode),并插入到parent中,在初次渲染时,before是不存在的,所以直接插入到parent中 - 如果不是真实的
DOM节点,就递归调用insertOrAppendPlacementNode函数- 如果不是真实的
DOM节点,说明现在处理的是组件,拿到组件的child,然后递归调用insertOrAppendPlacementNode函数
- 如果不是真实的
js
// react-reconciler/src/ReactFiberCommitWork.js
function insertOrAppendPlacementNode(fiber, before, parent) {
const isHost = isHostParent(fiber);
if (isHost) {
const { stateNode } = fiber;
if (!before) {
// stateNode 是一颗完整的树,只需要将它插入到 div#root 中就可以了,也就是这里的 parent
appendInitialChild(parent, stateNode);
}
} else {
const { child } = fiber;
if (child !== null) {
insertOrAppendPlacementNode(child, before, parent);
}
}
}
调用 appendInitialChild 函数将 node 插入到 parent 中
js
// react-dom-bindings/src/client/ReactDOMHostConfig.js
function appendInitialChild(parent, child) {
parent.appendChild(child);
}
初始渲染时 commitWork 阶段到此就执行完成了,页面已经渲染成功了
总结
在初始渲染时,commitWork 其实只处理了两个 Fiber:RootFiber 和 RootFiber.child,也就是 div#root 和 element/Component 对应的 Fiber
为什么 commitWork 只处理这两个 Fiber,就能将页面渲染出来呢?
因为在 completeWork 节点,每个 Fiber 的真实节点已经被挂载到它的父元素的真实 DOM 上了,所以在 commitWork 阶段,只需要将 element/Component 直接挂载到 div#root 上就可以了
commitWork 阶段的执行过程如下:
