深入学习React源码实现之Fiber副作用收集
一、Fiber副作用收集机制历史介绍
在 React 的 Stack Reconciler 时代(React 15 及之前),更新是同步的,组件树递归渲染过程中直接进行 DOM 更新操作。这种方式虽然简单,但缺乏中断和恢复能力,无法应对复杂应用中对性能和响应性的需求。
随着 React 16 引入 Fiber 架构,副作用收集成为协调器的重要组成部分。Fiber 将整个更新过程拆分为多个阶段:render phase
和 commit phase
,其中:
render phase
负责构建新的 Fiber 树并标记需要执行的副作用。commit phase
负责将这些副作用批量执行到真实 DOM 上。
这种设计使得 React 可以更灵活地调度任务,并支持并发模式(Concurrent Mode)下的中断与优先级调度。
相关源码文件
ReactFiberCommitWork.old.js
:定义了副作用执行的具体逻辑。ReactFiberWorkLoop.old.js
:工作循环负责触发副作用收集。ReactFiberEffects.old.js
:处理副作用队列的收集和清理。
二、算法设计思路和详细步骤
1. 副作用收集目标
副作用(Effect)是指那些在渲染后需要执行的操作,主要包括:
Placement
(插入)Update
(更新)Deletion
(删除)
React 在 completeUnitOfWork()
阶段通过 completeWork()
方法标记这些副作用,并将其添加到父节点的 effectList
中,最终在 commitRoot()
阶段统一执行。
2. 算法流程图解
beginWork()
↓
completeWork() ——> 收集当前节点副作用
↓
appendAllChildren() / reconcileChildren()
↓
递归完成所有子节点的 completeWork()
↓
将当前节点 effect 添加到父节点 effectList 中
↓
commitRoot()
↓
遍历 effectList 执行副作用
3. 关键步骤详解
步骤 1:completeWork() 收集副作用
这段代码是 React Fiber 架构中 completeWork
函数的简化实现,负责完成 Fiber 节点的处理工作。下面是对代码的详细注释:
/**
* 完成 Fiber 节点的处理工作
* @param {Fiber | null} current 当前 Fiber 节点(可能为 null)
* @param {Fiber} workInProgress 工作进中的 Fiber 节点
* @returns {Fiber | null} 返回下一个要处理的兄弟节点或 null
*/
function completeWork(current: Fiber | null, workInProgress: Fiber): Fiber | null {
const newProps = workInProgress.pendingProps;
// 根据 Fiber 节点的类型执行不同的处理逻辑
switch (workInProgress.tag) {
case HostComponent: { // 处理 DOM 组件节点
const type = workInProgress.type; // DOM 标签名(如 'div', 'span' 等)
if (current !== null && current.stateNode != null) {
// 如果是更新阶段,更新 DOM 属性
updateHostComponent(
current,
workInProgress,
type,
newProps
);
} else {
// 如果是初次创建,创建 DOM 实例
const instance = createInstance(
type,
newProps,
rootContainerInstance, // 根容器实例
hostContext, // 宿主环境上下文
internalInstanceHandle // 内部实例句柄
);
// 附加所有子节点到父实例
appendAllChildren(
instance,
workInProgress,
false, // 是否为 DOM 属性
false // 是否为隐藏的(如 CSS hidden)
);
// 设置 Fiber 节点的 stateNode 为创建的 DOM 实例
workInProgress.stateNode = instance;
// 初始化子节点的属性(如自动聚焦等)
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
hostContext
);
}
break;
}
case HostText: { // 处理文本节点
const newText = newProps; // 文本内容就是 props
if (current && current.stateNode !== null) {
// 如果是更新阶段,检查文本内容是否变化
const oldText = current.memoizedProps;
if (oldText !== newText) {
markWorkInProgressReceivedUpdate(); // 标记有更新需要处理
}
} else {
// 如果是初次创建,创建文本实例
const instance = createTextInstance(
newText,
rootContainerInstance,
hostContext,
internalInstanceHandle
);
// 设置 Fiber 节点的 stateNode 为创建的文本实例
workInProgress.stateNode = instance;
}
break;
}
default:
// 其他类型的节点不做特殊处理
break;
}
// 处理完当前节点后,决定下一个要处理的节点
if (workInProgress.child !== null) {
// 如果有子节点,继续向下处理子节点
return workInProgress.child;
} else {
// 如果没有子节点,向上查找兄弟节点
let node: Fiber | null = workInProgress;
while (node !== null) {
if (node.sibling !== null) {
// 找到兄弟节点,返回它
return node.sibling;
}
if (node.return === null) {
// 如果没有父节点,返回 null(到达根节点)
return null;
}
// 继续向上查找
node = node.return;
}
return null;
}
}
关键点说明:
-
节点类型处理:
HostComponent
:处理 DOM 元素节点HostText
:处理文本节点- 其他类型节点不做特殊处理
-
DOM 组件处理流程:
- 更新阶段:调用
updateHostComponent
更新 DOM 属性 - 创建阶段:
- 调用
createInstance
创建 DOM 实例 - 调用
appendAllChildren
附加所有子节点 - 调用
finalizeInitialChildren
初始化子节点属性
- 调用
- 更新阶段:调用
-
文本节点处理流程:
- 更新阶段:检查文本内容是否变化
- 创建阶段:调用
createTextInstance
创建文本节点
-
遍历逻辑:
- 深度优先遍历 Fiber 树
- 优先处理子节点
- 没有子节点时查找兄弟节点
- 没有兄弟节点时向上回溯
-
返回值:
- 返回下一个要处理的节点(可能是子节点或兄弟节点)
- 到达根节点时返回 null
这个函数是 Fiber 架构中协调(reconciliation)过程的重要组成部分,负责将 Fiber 树转换为实际的 DOM 结构。
步骤 2:提交副作用(commitRoot)
这段代码是 React Fiber 架构中 commitRoot
函数的简化实现,负责将工作完成(finished)的 Fiber 树提交到 DOM。下面是对代码的详细注释:
/**
* 提交根节点及其副作用到 DOM
* @param {FiberRoot} root Fiber 根节点
*/
function commitRoot(root) {
// 获取已完成的工作(即 workInProgress 树的根节点)
const finishedWork = root.finishedWork;
// 获取已完成的工作对应的更新优先级(lane)
const lanes = root.finishedLanes;
// 重置 finishedWork 和 finishedLanes,准备下一次更新
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 提交所有副作用(如 DOM 插入、更新、删除等)
commitMutationEffects(finishedWork, root);
// 将当前指针(current)切换到已完成的工作(即 workInProgress 树)
root.current = finishedWork;
}
关键点说明:
-
参数:
root
:Fiber 根节点对象,包含整个 Fiber 树的信息
-
获取已完成的工作:
finishedWork
:表示已经完成协调(reconciliation)过程的 Fiber 树根节点lanes
:表示已完成工作的更新优先级(在 React 的并发模式下使用)
-
重置状态:
- 将
finishedWork
和finishedLanes
重置为初始状态,为下一次更新做准备
- 将
-
提交副作用:
commitMutationEffects
:执行所有挂起的副作用(如 DOM 操作)- 这包括实际的 DOM 插入、更新和删除操作
- 这个函数会遍历整个 Fiber 树,处理所有标记了副作用的节点
-
切换当前指针:
- 将
root.current
指向finishedWork
,表示现在finishedWork
树成为新的当前树 - 这意味着下一次渲染将从这棵树开始
- 将
-
副作用类型:
- 在
commitMutationEffects
中会处理多种副作用,如:Placement
:插入新节点Update
:更新现有节点Deletion
:删除节点
- 在
这个函数是 React 渲染过程的最后阶段,负责将协调阶段产生的变更实际应用到 DOM 上。在 React 的并发模式下,这个阶段是不可中断的,以确保 UI 的一致性。
步骤 3:执行副作用(commitMutationEffects)
这段代码是 React Fiber 架构中 commitMutationEffects
函数的简化实现,负责处理所有挂起的副作用(如 DOM 插入、更新和删除)。下面是对代码的详细注释:
/**
* 提交所有挂起的副作用(如 DOM 插入、更新和删除)
* @param {Fiber} finishedWork 已完成协调的 Fiber 节点
* @param {FiberRoot} root Fiber 根节点
*/
function commitMutationEffects(finishedWork: Fiber, root: FiberRoot) {
// 从 finishedWork 的第一个副作用节点开始遍历
let nextEffect: Fiber | null = finishedWork.firstEffect;
// 遍历所有带有副作用的 Fiber 节点
while (nextEffect !== null) {
// 获取当前节点的副作用标记
const effectTag = nextEffect.effectTag;
// 处理 Placement 副作用(插入新节点)
if ((effectTag & Placement) !== NoEffect) {
commitPlacement(nextEffect); // 执行实际的 DOM 插入操作
nextEffect.effectTag &= ~Placement; // 清除 Placement 标记
}
// 处理 Update 副作用(更新现有节点)
if ((effectTag & Update) !== NoEffect) {
commitWork(nextEffect, nextEffect.alternate); // 执行实际的 DOM 更新操作
nextEffect.effectTag &= ~Update; // 清除 Update 标记
}
// 处理 Deletion 副作用(删除节点)
if ((effectTag & Deletion) !== NoEffect) {
commitDeletion(nextEffect); // 执行实际的 DOM 删除操作
nextEffect.effectTag &= ~Deletion; // 清除 Deletion 标记
}
// 移动到下一个带有副作用的节点
nextEffect = nextEffect.nextEffect;
}
}
关键点说明:
-
副作用遍历:
- 从
finishedWork.firstEffect
开始遍历所有带有副作用的 Fiber 节点 - 使用
nextEffect
指针在副作用链表中移动
- 从
-
副作用类型处理:
- Placement:处理 DOM 插入操作
- 调用
commitPlacement
执行实际的插入 - 清除
Placement
标记
- 调用
- Update:处理 DOM 更新操作
- 调用
commitWork
执行实际的更新 - 清除
Update
标记
- 调用
- Deletion:处理 DOM 删除操作
- 调用
commitDeletion
执行实际的删除 - 清除
Deletion
标记
- 调用
- Placement:处理 DOM 插入操作
-
副作用标记清除:
- 每种副作用处理完成后,使用位操作清除对应的标记
- 这是为了确保副作用只被处理一次
-
副作用链表:
- Fiber 节点通过
nextEffect
指针形成一个链表 - 这个链表是在协调(reconciliation)阶段构建的
- Fiber 节点通过
-
不可中断性:
- 这个阶段在 React 的并发模式下是不可中断的
- 确保所有副作用要么全部提交,要么全部不提交
这个函数是 React 渲染过程的最后阶段之一,负责将协调阶段产生的变更实际应用到 DOM 上。在 React 的并发模式下,这个阶段是不可中断的,以确保 UI 的一致性。
三、完整代码实现和注释
以下是一个简化版的副作用收集模拟实现:
// 模拟 Fiber 对象结构
class Fiber {
constructor(tag, key, elementType, pendingProps, mode) {
this.tag = tag; // 类型:FunctionComponent, ClassComponent 等
this.key = key; // 用于列表比对
this.elementType = elementType;// 元素类型(如函数组件、类组件等)
this.pendingProps = pendingProps;// 待处理的 props
this.mode = mode; // 渲染模式(如 ConcurrentMode)
this.alternate = null; // 指向另一个树的节点
this.child = null; // 第一个子节点
this.sibling = null; // 下一个兄弟节点
this.return = null; // 父节点
this.stateNode = null; // DOM 或组件实例
this.memoizedState = null; // 状态快照
this.updateQueue = null; // 更新队列
this.effectTag = 'NoEffect';// 副作用标记(如 Placement, Update, Deletion)
this.nextEffect = null; // 副作用链表中的下一个节点
this.firstEffect = null; // 副作用链表中的第一个节点
this.lastEffect = null; // 副作用链表中的最后一个节点
}
addEffect(effectTag) {
const effect = { effectTag, fiber: this };
if (!this.parent) return;
if (!this.parent.firstEffect) {
this.parent.firstEffect = effect;
this.parent.lastEffect = effect;
} else {
this.parent.lastEffect.nextEffect = effect;
this.parent.lastEffect = effect;
}
}
}
// 模拟 beginWork
function beginWork(current, workInProgress) {
console.log('Begin work on:', workInProgress.key || 'root');
return workInProgress.child;
}
// 模拟 completeWork 并收集副作用
function completeWork(workInProgress) {
console.log('Complete work on:', workInProgress.key || 'root');
// 模拟不同类型的节点处理
switch (workInProgress.tag) {
case 'HostComponent':
if (!workInProgress.stateNode) {
// 新增节点
workInProgress.effectTag = 'Placement';
workInProgress.addEffect(workInProgress.effectTag);
} else {
// 更新节点
workInProgress.effectTag = 'Update';
workInProgress.addEffect(workInProgress.effectTag);
}
break;
case 'HostText':
if (!workInProgress.stateNode) {
workInProgress.effectTag = 'Placement';
workInProgress.addEffect(workInProgress.effectTag);
}
break;
default:
break;
}
}
// 模拟 commit 阶段
function commitMutationEffects(finishedWork) {
console.log('Committing effects...');
let effect = finishedWork.firstEffect;
while (effect) {
switch (effect.effectTag) {
case 'Placement':
console.log(`Place node: ${effect.fiber.key}`);
break;
case 'Update':
console.log(`Update node: ${effect.fiber.key}`);
break;
case 'Deletion':
console.log(`Delete node: ${effect.fiber.key}`);
break;
default:
break;
}
effect = effect.nextEffect;
}
}
// 主工作循环
function performUnitOfWork(unitOfWork) {
const current = unitOfWork.alternate;
const next = beginWork(current, unitOfWork);
if (next === null) {
completeWork(unitOfWork);
}
return next;
}
function workLoop(isYieldy) {
let unitOfWork = workInProgressFiber;
while (unitOfWork !== null) {
unitOfWork = performUnitOfWork(unitOfWork);
}
if (!isYieldy && unitOfWork === null) {
commitMutationEffects(workInProgressFiber);
}
}
let currentFiber = new Fiber('HostRoot', null, null, null, 'ConcurrentMode');
let workInProgressFiber = null;
function createWorkInProgressFromCurrent(current) {
if (!current.alternate) {
workInProgressFiber = new Fiber(
current.tag,
current.key,
current.elementType,
current.pendingProps,
current.mode
);
workInProgressFiber.alternate = current;
current.alternate = workInProgressFiber;
} else {
workInProgressFiber = current.alternate;
workInProgressFiber.pendingProps = current.pendingProps;
workInProgressFiber.effectTag = 'NoEffect';
}
return workInProgressFiber;
}
function scheduleUpdate() {
workInProgressFiber = createWorkInProgressFromCurrent(currentFiber);
workLoop(false);
}
scheduleUpdate();
四、设计模式分析
设计模式 | 应用场景 |
---|---|
观察者模式 | effectTag 表示要执行的副作用,由 completeWork 观察并收集 |
链表模式 | firstEffect 和 lastEffect 形成副作用链表,便于 commit 阶段遍历 |
策略模式 | 不同类型 Fiber(如 HostComponent、FunctionComponent)使用不同副作用处理策略 |
模板方法模式 | performUnitOfWork() 提供通用流程,beginWork 和 completeWork 是具体实现 |
五、10大Fiber副作用收集高频面试题
-
什么是 React 的副作用?常见的副作用有哪些?
- 副作用是指在 render 阶段完成后需要执行的 DOM 操作,如
Placement
,Update
,Deletion
。
- 副作用是指在 render 阶段完成后需要执行的 DOM 操作,如
-
副作用是如何被收集的?在哪里被记录?
- 在
completeWork()
阶段通过effectTag
标记,并加入到父节点的firstEffect
链表中。
- 在
-
为什么要在 render 阶段收集副作用,而不是直接操作 DOM?
- 为了支持异步渲染和中断恢复,避免中间状态暴露给用户。
-
effectTag 的含义是什么?它是如何表示多个副作用的?
- 使用位掩码(bitmask)方式存储多个副作用,如
Placement | Update
。
- 使用位掩码(bitmask)方式存储多个副作用,如
-
副作用是如何执行的?执行顺序是怎样的?
- 在
commitRoot()
中遍历effectList
,按照深度优先顺序执行副作用。
- 在
-
effectList 是如何构建的?父子节点之间如何传递副作用?
- 子节点将副作用添加到父节点的
effectList
中,形成一个单向链表。
- 子节点将副作用添加到父节点的
-
React 如何处理嵌套组件的副作用?
- 递归调用
completeWork()
处理每个节点,确保所有子节点的副作用都被正确收集。
- 递归调用
-
为什么不能在 render 阶段直接修改 DOM?这样做会带来什么问题?
- 同步修改 DOM 会导致 UI 闪烁;异步渲染时可能因中断导致不一致状态。
-
React 如何优化副作用的执行效率?
- 批量执行副作用、使用位运算快速判断副作用类型、减少不必要的 DOM 操作。
-
在 Concurrent Mode 下,副作用是否有可能被多次执行?为什么?
- 是的,因为高优先级更新可能中断低优先级更新,导致部分副作用未提交而重新执行。
总结
Fiber 副作用收集机制是 React 实现高效、可中断、可恢复更新的关键部分。它通过 effectTag
和 effectList
的设计,在 render phase
安全地收集副作用,并在 commit phase
统一执行,保证了 DOM 更新的一致性和性能。理解这一机制有助于深入掌握 React 的内部运行原理,为性能优化和调试提供理论支撑。