本篇我们看下commit阶段工作过程部分,本系列文章旨在分析react核心模块内部运行原理和源码实现,提升架构和编码能力。
早期Effect list方式
在Fiber架构的早期阶段,并没有subtreeFlags,而是使用一种被称为Effect List的链表结构保存被标记副作用的fiberNode。在 completeWork 中,如果 fiberNode 存在副作用,就会被插入 Effects list 中。commit 阶段的三个子阶段只需遍历 Effects list 并对 fiberNode 执行 “flags 对应操作,既然遍历链表(Effects list)比遍历树(subtreeFlags)更高效,那么 React v18(下文简称 v18)为什么会用 subtreeFlags 替换 Effects list 呢?这是因为虽然 subtreeFlags 遍历子树的操作需要比 Effects list 遍历更多节点,但是 v18 中 Suspense 的行为恰恰需要遍历子树。Suspense 是 React v16 就已经提供的功能。但在 v18 开启并发更新后,Suspense 与之前版本的行为是有区别的。故而采取从根节点遍历fiber树取代了Effect list。
effectList 收集与执行的流程(含子节点优先逻辑)
1. 后序遍历收集 effectList(子节点先入链)
Fiber 树的遍历采用「深度优先后序遍历」,流程如下:
父节点
↓
遍历子节点 A → 遍历子节点 A 的子节点 → ...(直到叶子节点)
↓
处理子节点 A 的副作用(加入 effectList)
↓
遍历子节点 B → 遍历子节点 B 的子节点 → ...
↓
处理子节点 B 的副作用(加入 effectList)
↓
处理父节点的副作用(加入 effectList)
- 叶子节点的副作用最先被加入
effectList,然后是其父节点,最终形成「子 → 父」的链表顺序。
2. effectList 执行顺序(按链表顺序,子节点先执行)
effectList 是一个单向链表(通过 Fiber 节点的 nextEffect 属性串联),执行时从链表头部(第一个子节点)开始,依次向后遍历,直到链表尾部(根节点)。
示例流程:
effectList 结构:叶子节点 → 父节点 A → 父节点 B → 根节点
↓
执行顺序:
1. 叶子节点的副作用(如 DOM 插入、useEffect 清理/创建)
2. 父节点 A 的副作用
3. 父节点 B 的副作用
4. 根节点的副作用
代码层面的体现(简化逻辑)
在协调阶段的completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条被称为effectList的单向链表中。收集 effectList 的核心逻辑(后序遍历):
运行
function collectEffectList(fiber) {
if (fiber.child) {
// 先遍历子节点(深度优先)
collectEffectList(fiber.child);
}
// 子节点处理完后,再将当前节点加入 effectList(后序)
if (fiber.effectTag) { // 存在副作用标记(如更新、插入等)
if (lastEffect) {
lastEffect.nextEffect = fiber; // 链接到链表尾部
} else {
effectList = fiber; // 初始化链表头部
}
lastEffect = fiber;
}
if (fiber.sibling) {
// 遍历兄弟节点
collectEffectList(fiber.sibling);
}
}
- 由于先递归处理子节点,子节点会先被加入
effectList,因此执行时自然先执行子节点的副作用。
新commit阶段整个运行过程
Renderer工作的阶段被称为commit阶段。在commit阶段,会将各种副作用flags提交到宿主环境UI中,commit阶段一旦开始就会同步执行完毕。整个过程可以分为三个子阶段:
- before mutation阶段(执行
DOM操作前) - mutation阶段(执行
DOM操作) - layout阶段(执行
DOM操作后)
其中在commit阶段开始的时候,需要先判断本次更新是否涉及副作用更新,没有就直接跳过子阶段,有就进入子阶段的执行。
3个子阶段的运行流程可以用以下流程图表示(结合文字说明):
┌─────────────────────────────────────────────────────────────────┐
│ Commit 阶段开始 │
└───────────────────────────┬─────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 1. Before Mutation 阶段(DOM 变更前) │
│ ├─ 执行 `getSnapshotBeforeUpdate`(类组件生命周期) │
│ ├─ 清空HostRoot挂载的内容,方便Mutation阶段渲染 │
│ └─ 调度 `useLayoutEffect` 的销毁函数(标记为待执行) │
└───────────────────────────┬─────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 2. Mutation 阶段(DOM 变更) │
│ ├─ 根据 Fiber 树的变更,执行 DOM 操作(插入/删除/更新节点) │
│ ├─ 执行componentWillUnmount,useEffect和useLayoutEffect的destory方法 │
│ └─ divRef的卸载工作 │
└───────────────────────────┬─────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 3. Layout 阶段(DOM 变更后) │
│ ├─ 执行 `useLayoutEffect` 的创建函数(同步执行,依赖最新 DOM) │
│ ├─ 更新 `ref` 引用(指向最新的 DOM 节点) │
│ ├─ 执行 `componentDidMount`/`componentDidUpdate`(类组件) │
│ └─ 调度 `useEffect` 的创建函数(异步执行,不阻塞浏览器绘制) │
│ └─ setState和render的回调callback函数执行 │
└───────────────────────────┬─────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Commit 阶段结束 │
└─────────────────────────────────────────────────────────────────┘
其中Fiber Tree切换是在Mutation阶段的工作完成后,进入Layout阶段之前
root.current = finishedWork;
之所以选择在这个时机,因为对于ClassComponent,当执行componentWillUnmount时(Mutation阶段),Current Fiber Tree仍对应UI中的树。当执行componentDidMount/componentDidUpdate(Layout阶段),Current Fiber Tree已经应对本次更新的fiber树。