最近在学习慕课网 手写 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工作过程
正文开始:
上一篇我们把 beginWork 功能介绍完了,但是不意味着 beginWork 结束了,实际上 beginWork 和 completeWork 是交替进行的
beginWork 工作结束后,会返回一个 fiber 节点,这个节点会传给 completeWork
工作流程:


completeWork 和 beginWork 工作过程
在调用 completeWork 之前,先要梳理一下 react 是怎么遍历 fiber 树的
beginWork 和 completeWork 不是单独执行,而是交替执行的
js
let element = (
<div className="first">
<div className="first-1">first-1</div>
<div className="first-2">
text-1
<div className="second-21">second-21</div>
<div className="second-22">
<div className="third-221">third-221</div>
text-2
<div className="third-222">third-222</div>
</div>
<div className="second-23">second-23</div>
</div>
<div className="first-3">
text-3
<div className="second-31">second-31</div>
<div className="second-32">second-32</div>
</div>
</div>
);
const root = createRoot(document.getElementById("root"));
// 🔽
// 虚拟 DOM 结构
div#root
- div.first
- div.first-1
- div.first-2
- text-1
- div.second-21
- div.second-22
- div.third-221
- text-2
- div.third-222
- div.second-23
- div.first-3
- text-3
- div.second-31
- div.second-32
beginWork 是从 div.first 这个 DOM 开始向下遍历,它是深度遍历,同时只遍历它的第一个子节点,如果没有子节点就遍历结束
js
let next = beginWork(current, unitOfWork); // next 是第一个子 fiber,深度遍历
// 如果 next === null,说明没有子节点了,这次深度遍历结束
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
这里遍历时,几个变量不能搞混了,需要时刻搞清楚当前指向的是什么:
js
workInProgress: 是当前正在构建的 fiber 节点
unitOfWork: unitOfWork === workInProgress
next: 是当前正在构建的 fiber 节点的第一个子 fiber 节点,也就是 workInProgress.child
siblingFiber: 是当前正在构建的 fiber 节点的下一个兄弟 fiber 节点,也就是 workInProgress.sibling
我们来看下具体的遍历流程:
beginWork是从div.first开始遍历的,beginWork所遍历到的虚拟DOM,都会生成对应的fiber- 从
div.first开始深度遍历,遍历到div.first-1时发现没有子节点了,停止执行beginWork,这时next = null、workInProgress = div.first-1、completedWork = div.first-1 - 执行
completeWork,被completeWork处理的fiber都会创建真实的DOM节点,completeWork执行完之后,会查看当前fiber有没有兄弟节点,这时siblingFiber = div.first-2、workInProgress = div.first-2- 如果有兄弟节点,就执行
beginWork,对兄弟节点进行深度遍历 - 如果没有兄弟节点,就向上找父节点(父节点肯定已经执行过
beginWork),对父节点执行completeWork
- 如果有兄弟节点,就执行
- 执行
beginWork,对div.first-2进行深度遍历,遍历到text-1时发现没有子节点了,停止执行beginWork,这时next = null、workInProgress = text-1、completedWork = text-1 - 执行
completeWork,等到completeWork执行完之后,查看当前fiber有没有兄弟节点,这时siblingFiber = div.second-21、workInProgress = div.second-21 - 执行
beginWork,返现没有子节点,这是next = null、workInProgress = div.second-21、completedWork = div.second-21 completeWork,等到completeWork执行完之后,查看当前fiber有没有兄弟节点,这时siblingFiber = div.second-22、workInProgress = div.second-22- 执行
beginWork,对div.second-22进行深度遍历,遍历到div.third-221时发现没有子节点了,停止执行beginWork,这时next = null、workInProgress = div.third-221、completedWork = div.third-221 - 执行
completeWork,等到completeWork执行完之后,查看当前fiber有没有兄弟节点,这时由于已经到最底层的节点了,所以completeWork和beginWork会交替执行,直到没有兄弟节点为止,这时siblingFiber = null、completedWork = div.second-22(没有节点,需要对父节点执行completeWork) - 继续执行
completeWork,等到completeWork执行完之后,查看当前fiber有没有兄弟节点,这时siblingFiber = div.second-23、workInProgress = div.second-23 - 执行
beginWork,对div.second-23进行深度遍历,遍历到div.second-23时发现没有子节点了,停止执行beginWork,这时next = null、workInProgress = div.second-23、completedWork = div.second-23 - 执行
completeWork,等到completeWork执行完之后,查看当前fiber有没有兄弟节点,这时siblingFiber = null、completedWork = div.first-2 - 执行第 ⑩ 步,直到把
div.first的所有子节点都执行结束,这时siblingFiber = null、completedWork = div.first - 执行
completeWork,等到completeWork执行完之后,siblingsFiber = null、completedWork = div#root - 对
div#root执行completeWork,这时completedWork = null - 遍历结束
beginWork 和 completeWork 交替执行的流过程如下:

beginWork 执行的过程图如下:

completeWork 执行的过程图如下:

简化后的 beginWork 和 completeWork 执行过程

具体的实现逻辑如下:
js
// react-reconciler/src/ReactFiberWorkLoop.js
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork) {
const current = unitOfWork.alternate;
// 执行 beginWork
// next 是 beginWork 返回的第一个子 fiber
let next = beginWork(current, unitOfWork);
// 在经过 beingWork 处理之后,pendingProps 已经处理完了,可以赋值给 memoizedProps
unitOfWork.memoizedProps = unitOfWork.pendingProps;
// 如果 next === null,说明没有子节点了,本次深度遍历结束
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
// next 存在,说明子节点中也有子节点,继续循环调用 performUnitOfWork
workInProgress = next;
}
}
function completeUnitOfWork(unitOfWork) {
// completedWork 是接下来要执行 completeWork 的 fiber
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
// 当前处理的 fiber 的 父 fiber
const returnFiber = completedWork.return;
// 执行 completeWork
completeWork(current, completedWork);
// 当前 fiber 的兄弟节点
const sibling = completedWork.sibling;
// 如果 sibling 不为 null,说明兄弟节点还没有被 beginWork 处理,需要调用 beginWork,将兄弟从虚拟 DOM 转换成 fiber
if (sibling !== null) {
workInProgress = sibling;
return;
}
// 没有兄弟节点了,说明这个父节点的子节点都处理完了,那么就对父节点处理 completeWork
completedWork = returnFiber;
// do while 循环会一直执行,直到 completedWork 为 null
// 所以 workInProgress 就算有值,都不会执行 beginWork,直到退出 do while 循环,也就退出了 while 循环
// completedWork 为 null 时,上一个 fiber 是 div#root
workInProgress = completedWork;
} while (completedWork !== null);
}
为什么 completeWork 和 beginWork 要交替执行呢?
在 beginWork 执行结束后,虚拟 DOM 会转变成 Fiber,这时如果直接将 Fiber 转变成真实的 DOM 就会有问题
问题在于 Fiber 如果有子节点怎么办?
react 在处理完将虚拟 DOM 转变为 Fiber 后,先看一下这个 Fiber 有没有字节点,如果有子节点就执行子节点的 beginWork,如果没有子子节点就执行 completeWork,直到所有子节点都处理完,在对父 Fiber 执行 completeWork
completeWork
completeWork 函数的作用有三点:
- 创建真实的
DOM节点 - 将当前子节点下子节点挂载到当前节点上
- 收集当前节点下子节点的
flags和subtreeFlags
目前 DOM 节点有三种:
-
HostRoot:是RootFiber,它的stateNode有真实的节点,所以不需要处理 -
HostComponent:是普通的DOM,这是最复杂的部分,具体处理过程查看HostComponent章节 -
HostText:是文本节点,我们需要创建一个文本节点,由createTextInstance创建- 这个文本节点指的是
text-1
js<div> text-1 <div>text-2</div> </div> - 这个文本节点指的是
最后这三个节点还都需要处理一件事:属性冒泡 ,由 bubbleProperties 完成,具体查看 bubbleProperties 章节
js
// react-reconciler/src/ReactFiberCompleteWork.js
function completeWork(current, workInProgress) {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case HostRoot:
// 收集当前节点下子节点的 flags 和 subtreeFlags
bubbleProperties(workInProgress);
break;
case HostComponent:
const { type } = workInProgress;
// 创建真实 DOM 节点
const instance = createInstance(type);
// 将子节点挂载到当前节点上
appendAllChildren(instance, workInProgress);
// 将真实 DOM 节点挂载到当前 fiber 的 stateNode 属性上
workInProgress.stateNode = instance;
// 将属性挂载到真实 DOM 节点上
finalizeInitialChildren(instance, type, newProps);
// 收集当前节点下子节点的 flags 和 subtreeFlags
bubbleProperties(workInProgress);
break;
case HostText:
const nextText = newProps;
workInProgress.stateNode = createTextInstance(nextText);
// 收集当前节点下子节点的 flags 和 subtreeFlags
bubbleProperties(workInProgress);
break;
default:
break;
}
return null;
}
HostComponent
处理 HostComponent 节点,需要做哪些事情呢?
- 创建一个真实节点,由
createInstance完成 - 追加自己所有的子节点,由
appendAllChildren完成 - 然后把创建的节点挂载到当前
fiber的stateNode属性上 - 把属性挂载到真实节点上,由
finalizeInitialChildren完成
js
const { type } = workInProgress;
// 创建真实 DOM 节点
const instance = createInstance(type);
// 将子节点挂载到当前节点上
appendAllChildren(instance, workInProgress);
// 将真实 DOM 节点挂载到当前 fiber 的 stateNode 属性上
workInProgress.stateNode = instance;
// 将属性挂载到真实 DOM 节点上
finalizeInitialChildren(instance, type, newProps);
appendAllChildren
appendAllChildren 作用是找到真实 DOM 节点,然后将子节点添加到父节点上,具体的添加由 appendInitialChildren 完成
appendAllChildren 只处理 workInProgress 的子节点子节点,如果直接子节点没有 child,才在往下处理,否则不处理
-
子节点是真实节点,直接添加到父节点上;子节点是组件,结束当前循环,进入下一次循环
js// 子节点是真实节点 if (node.tag === HostComponent || node.tag === HostText) { appendInitialChildren(parent, node.stateNode); } else if (node.child !== null) { // 子节点是组件 node = node.child; continue; } -
子节点有没有兄弟节点
- 有兄弟节点,则继续处理兄弟节点
- 没有兄弟节点,则看下父节点是不是
workInProgress,如果不是,说明当前的节点是个组件,还需要在往上找到真正的父节点
js// 子节点没有兄弟节点 while (node.sibling === null) { if (node.return === null || node.return === workInProgress) { // DOM 走这里 return; } // 处理的是组件 node = node.return; } // 子节点有兄弟节点 node = node.sibling;
具体的流程图如下:

js
// react-reconciler/src/ReactFiberCompleteWork.js
function appendAllChildren(parent, workInProgress) {
// 拿到子节点
let node = workInProgress.child;
// 循环子节点
while (node) {
// 如果子节点是 HostComponent 或者 HostText,就追加到父节点上
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChildren(parent, node.stateNode);
} else if (node.child !== null) {
// 子节点是组件
// 组件没有真实的 DOM 节点,组件的真实节点在 child 上
node = node.child;
// 子节点是组件就不往下处理了,直接进入下一次循环
continue;
}
// 这一步也很关键
// 这一步和 while (node.sibling === null) node = node.return; 配合使用
// 如果父节点是组件的话,继续往上找,还是跳出循环
if (node === workInProgress) {
return;
}
// 子节点没有兄弟节点
while (node.sibling === null) {
// 这一步也很关键
// 如果子节点没有兄弟节点,就看下当前节点的父节点是不是正在执行 completeWork 的节点,避免重复处理
if (node.return === null || node.return === workInProgress) {
// DOM 走这里
return;
}
// 处理的是组件
node = node.return;
}
// 子节点有兄弟节点
node = node.sibling;
}
}
bubbleProperties
什么是属性冒泡?
当执行 completeWork 时,需要将记录当前 Fiber 的子 Fiber 的 flags 和 subtreeFlags 属性
我们知道 flags 记录的是当前 Fiber 有哪些操作,subtreeFlags 记录的是当前 Fiber 的子 Fiber 有哪些操作
react 这么做的好处是,只需要遍历当前 Fiber 的直接子节点,就可以知道当前 Fiber 下所有子节点有哪些操作,而不需要遍历所有子节点
bubbleProperties 工作过程:

js
// react-reconciler/src/ReactFiberCompleteWork.js
function bubbleProperties(completedWork) {
// NoFlags 表示没有变化
let subtreeFlags = NoFlags;
// 拿到第一个子 fiber
let child = completedWork.child;
while (child !== null) {
// subtreeFlags 保存 child.child 有没有变化
subtreeFlags |= child.subtreeFlags;
// flags 保存 child 有没有变化
subtreeFlags |= child.flags;
// 拿到 child.sibling 节点
child = child.sibling;
}
// 将收集到的 flags 保存到 completedWork 上
completedWork.subtreeFlags = subtreeFlags;
}
其他函数
这些函数都比较简单,都是一些 DOM 操作,react 将其放在 react-dom-bindings 包中
finalizeInitialChildren
finalizeInitialChildren 函数最终调用的是 setInitialDOMProperties 函数,这个函数会将 fiber 的 props 属性挂载到真实的 DOM 节点上
setTextContent 函数是处理文本节点,你可能想说之前不是有一个 HostText 处理分支吗,怎么这里又要处理文本节点了,具体的区别在 setTextContent 和 createTextInstance 章节有讲到
js
// react-dom-bindings/src/client/ReactDOMHostConfig.js
function finalizeInitialChildren(domElement, type, props) {
setInitialProperties(domElement, type, props);
}
// react-dom-bindings/src/client/ReactDomComponent.js
function setInitialProperties(domElement, tag, props) {
setInitialDOMProperties(tag, domElement, props);
}
function setInitialDOMProperties(tag, domElement, nextProps) {
// 遍历属性
for (const propKey in nextProps) {
// 只处理 props 上自身的属性
if (nextProps.hasOwnProperty(propKey)) {
const nextProp = nextProps[propKey];
// 处理 style
if (propKey === "style") {
setValueForStyles(domElement, nextProp);
} else if (propKey === "children") {
// 如果是 children 属性,且是文本节点,就设置文本内容
/**
* <div>
* text-1
* <div>text-2</div>
* </div>
*/
// 处理 "text-2" 节点
if (typeof nextProp === "string" || typeof nextProp === "number") {
setTextContent(domElement, `${nextProp}`);
}
} else {
// 处理其他属性
setValueForProperty(domElement, nextProp);
}
}
}
}
appendInitialChildren
appendInitialChildren 函数是给父节点追加子节点
js
// react-dom-bindings/src/client/ReactDOMHostConfig.js
function appendInitialChildren(parent, child) {
parent.appendChild(child);
}
setValueForStyles
给 DOM 节点设置 style 属性
js
// react-dom-bindings/src/client/CSSPropertyOperations.js
function setValueForStyles(node, styles) {
const { style } = node;
for (const styleName in styles) {
if (styles.hasOwnProperty(styleName)) {
const styleValue = styles[styleName];
style[styleName] = styleValue;
}
}
}
setValueForProperty
有值就设置属性,没有值就移除属性
js
// react-dom-bindings/src/client/DOMPropertyOperations.js
function setValueForProperty(node, name, value) {
if (value == null) {
node.removeAttribute(name);
} else {
node.setAttribute(name, value);
}
}
createInstance
创建一个真实的 DOM 节点
js
// react-dom-bindings/src/client/ReactDOMHostConfig.js
function createInstance(type) {
return document.createElement(type);
}
setTextContent 和 createTextInstance
这两个函数要着重讲下区别
setTextContent函数作用是设置文本内容createTextInstance函数作用是创建文本内容
先看一段 jsx 代码:
js
<div className="first">
text-1
<div className="second">text-2</div>
</div>
react 对 text-1 文本的处理是调用 createTextInstance,对 text-2 文本的处理是调用 setTextContent
因为对 react 来说,text-1 是一个 Fiber 节点,而 text-2 对应的 Fiber 是 div.second
为什么 react 要这么处理呢?
因为在 beginWork 处理时,对于 div.first 节点来说 children 是一个数组,而是 div.second 节点来说 children 是个文本
所以 react 会有两种文本处理方式
js
// react-dom-bindings/src/client/setTextContent.js
// 这个函数只是修改 DOM 节点的 textContent 属性
// 也就是说给 <div className="second">text-2</div> 这种节点设置文本内容
function setTextContent(node, text) {
node.textContent = text;
}
// react-dom-bindings/src/client/ReactDOMHostConfig.js
// 这个函数是对 text-1 创建文本节点
function createTextInstance(content) {
return document.createTextNode(content);
}
总结
beginWork和completeWork交替执行- 一个节点在执行
completeWork前,需要先执行beginWork - 一个节点在执行
completeWork时,需要处理它的子Fiber - 父
Fiber要等到子Fiber都执行完了completeWork后才会执行
- 一个节点在执行
completeWork执行Fiber是workInProgress,只处理workInProgress的直接子节点,不处理子节点的子节点- 如果
Fiber是组件的话,由于组件没有子节点,所以需要找到组件的子节点,然后再处理子节点
- 如果
- 收集子节点的
flags和subtreeFlags,放在workInProgress的subtreeFlags上
源码
beginWork和completeWork交替执行- appendAllChildren
- bubbleProperties