React 19.3.1 源码深度剖析-第 3 章 Fiber 架构与双缓冲
全系列最底层的地基。本章不依赖任何上层概念,完全从
packages/react-reconciler/src的真实源码出发,把"纤程(Fiber)"这个 React 19.3.1 的中枢数据结构,以及它背后的"双缓冲(Double Buffering)"机制彻底讲透。读完这一章,你对 React 渲染调度、并发、优先级、内存模型的理解,会从一个模糊的"知道有 Fiber"变成可以精确指到某一行代码的程度。
一、动机与背景:Fiber 不是凭空出现的
很多人听说过 Fiber,但很少有人讲清楚:为什么 React 15 的 Stack Reconciler(栈协调器)必须被推翻?为什么新的架构非得是一棵"链表"而不是一棵"对象树"?本节先把这两个问题钉死,因为它们决定了 Fiber 的全部设计取舍。
1.1 React 15 的 Stack Reconciler 之痛
在 React 15 及更早的版本里,协调(reconciliation,即 Diff)过程是一棵递归树:从根组件开始,render 出子节点,再对子节点递归 render,直到整棵树被遍历完。整个过程是同步、不可中断、深度优先的。
这种"栈式递归"有两个根本无法克服的缺陷:
- 一旦开始就停不下来 。
render是递归调用,调用栈一层层压下去,中间没有任何"让出主线程"的时机。如果组件树很大(比如几千个节点),这一次渲染会长时间占用主线程,导致动画掉帧、输入框卡顿、点击无响应。用户感受到的就是"页面卡死了"。 - 优先级无从谈起。因为是一条道走到黑,所有更新都被"一视同仁"地一次性处理,无法区分"用户正在输入的紧急更新"和"后台预取的非紧急更新"。
要根治这两个问题,React 团队意识到:必须把递归变成可中断的循环 ,并且把"正在进行的渲染"和"已经提交的结果"分离------前者可以被打断、可以被复用,后者永远保持可用。这两点,正是 Fiber 与双缓冲的由来。
1.2 Fiber 的两个核心目标
React 团队在 Fiber 设计文档里明确写了两个目标(这里我们结合源码印证,而非空谈):
- 可中断与可恢复(Interruptible & Resumable) :渲染工作被拆成一个个"工作单元(Unit of Work)",每个单元处理一个 Fiber 节点。处理完一个单元后,React 可以决定是继续下一个,还是把控制权交还主线程。这要求"渲染状态"能被保存下来------
alternate指针与workInProgressRoot等全局变量就是干这个的。 - 分优先级调度(Priority-aware Scheduling) :不同的更新被赋予不同的"车道(Lane)",高优先级的更新可以打断低优先级的渲染。这要求优先级的表示、比较、合并都必须高效且可组合,于是 React 16.13 之后从
expirationTime(时间戳模型)演进出 31 位位掩码(bitmask)模型------也就是本章第六节要逐位拆解的 Lane 系统。
1.3 一个常被忽略的事实:Fiber 是"单元 + 节点"的合体
在 ReactInternalTypes.js:89 的 Fiber 类型定义里,有一段注释极其关键(我贴出来,后面会反复引用):
js
// These first fields are conceptually members of an Instance. This used to
// be split into a separate type and intersected with the other Fiber fields,
// but until Flow fixes its intersection bugs, we've merged them into a
// single type.
意思是:Fiber 节点同时承载了"组件实例(Instance)"和"工作单元(Unit of Work)"两种身份 。前半部分的 tag/key/elementType/type/stateNode 是"实例"信息(一个组件在所有版本里共享),后半部分的 return/child/sibling/flags/lanes/alternate 是"工作单元"信息。把它们合并到同一个对象上,是为了"minimize the number of objects created during the initial render"(最小化初始渲染时创建的对象数量)。这一点直接关系到双缓冲的内存策略,后面讲 createWorkInProgress 时会再次印证。
二、整体心智模型:一句话 + 一张图
一句话 :Fiber 是一棵由"单向链表"(child / sibling / return)而非"递归对象嵌套"构成的虚拟 DOM 树,每个节点都是可中断渲染的最小工作单元;React 用 current 与 workInProgress 两棵树互为"替身"(alternate),在后台构建新树、构建完成后再一次性切换指针,从而让"可中断的并发渲染"与"永远可用的已提交页面"并存。
下面这张图给出宏观结构(注意 return 是"父",child 是"第一个子",sibling 是"下一个兄弟",三者构成的是一棵以数组式链表表达的树,而非 JS 对象天然嵌套的树):
注意图里的箭头语义:child / sibling 是"向下/向右"的实线(遍历方向),return 是"向上"的虚线(回溯方向)。一个 Fiber 节点知道自己唯一的父(return),但父不知道自己有几个子------父只持有一个 child 指针指向第一个孩子,其余孩子靠 sibling 串成链表。这是理解整个 work loop 的起点。
三、核心数据结构(一):Fiber 节点逐字段讲解
这一节是本章的"地基中的地基"。我们逐字段拆解 ReactFiber.js:134 的 FiberNode 构造函数和 ReactFiber.js:232 的 createFiberImplObject(后者在 enableObjectFiber 开关打开时使用对象字面量,前者使用 new 构造;两者字段完全一致,源码里 createFiber = enableObjectFiber ? createFiberImplObject : createFiberImplClass)。我会按照"实例字段"和"工作单元字段"两大类组织,并注明每个字段的类型、作用、何时赋值、与谁关联。
3.1 实例字段(Instance)
这些字段描述的是"这个组件本身是什么",在两棵树(current / workInProgress)之间通常是共享 的(通过 createWorkInProgress 直接拷贝引用)。
3.1.1 tag: WorkTag
js
this.tag = tag; // ReactFiber.js:142
tag 是一个数字枚举(定义在 ReactWorkTags.js),标识这个 Fiber 是哪一类节点:HostRoot(3)、FunctionComponent(0)、ClassComponent(1)、HostComponent(5)(真实 DOM 节点)、HostText(6)、Fragment(14)、ContextProvider、SuspenseComponent、OffscreenComponent 等等。
它的关键作用:beginWork / completeWork 在协调阶段正是通过 switch (workInProgress.tag) 来决定"这个节点该怎么处理" 。不同的 tag 走完全不同的协调逻辑------函数组件要跑 Hooks,类组件要调 render,HostComponent 要创建/更新真实 DOM。所以 tag 是渲染分发的"总开关"。
3.1.2 key: ReactKey
js
this.key = key; // ReactFiber.js:143
key 用于列表 Diff 时识别同一位置的兄弟节点身份 。它和 elementType / type 一起参与"是否复用同一 Fiber"的判断(在 ReactChildFiber.js 的协调逻辑中)。值得注意:在 createWorkInProgress 里,key 默认会被从 current 拷贝到 workInProgress(ReactFiber.js:368,仅在 enableOptimisticKey 开关下显式赋值,否则在 410 行 workInProgress.sibling = current.sibling 等整体拷贝中已包含)。
3.1.3 elementType 与 type
js
this.elementType = null; // ReactFiber.js:144
this.type = null; // ReactFiber.js:145
这两个字段容易混淆,源码注释说得很清楚:
elementType:是"React 元素上的type",即你写的 JSX 里那个东西------可能是函数、类、字符串(host 组件)、或者React.memo/React.forwardRef包裹后的对象。type:解析后的type。"The resolved function/class associated with this fiber"。对于函数组件,它最终是那个函数本身;对于React.memo包裹的组件,elementType是Memo对象,type是里层的真实组件函数。
为什么需要两个?因为协调阶段有些判断要用"原始描述"(elementType,例如开发时的热更新 resolveTypeForHotReloading),有些要用"真实实现"(type)。在 createFiberFromTypeAndProps(ReactFiber.js:728-729)里可以看到赋值:
js
const fiber = createFiber(fiberTag, pendingProps, key, mode);
fiber.elementType = type;
fiber.type = resolvedType;
3.1.4 stateNode
js
this.stateNode = null; // ReactFiber.js:146
stateNode 保存"与这个 Fiber 关联的本地状态实例",它的含义随 tag 而变:
HostRoot(根节点):stateNode指向整个FiberRoot对象 (见ReactFiberRoot.js:212uninitializedFiber.stateNode = root)。这是一个关键回路------从任意 Fiber 出发,沿return一路向上到 HostRoot,再读.stateNode就能拿到FiberRoot。- 类组件:
stateNode是该类组件的实例 (那个new MyClass()出来的对象),组件上的this.state、this.refs都在这里。 - HostComponent(真实 DOM):
stateNode是浏览器里的真实 DOM 节点。 - FunctionComponent:通常是
null(函数组件没有实例,Hooks 状态存在memoizedState链表上,不在这里)。
createWorkInProgress 在创建替身时会直接共享 stateNode(ReactFiber.js:339 workInProgress.stateNode = current.stateNode),因为它们指向同一个实例/DOM 节点,没必要复制。
3.2 工作单元字段(Fiber)
这些字段描述的是"这次渲染工作进行到哪了",在 current 和 workInProgress 之间通常不共享,需要各自维护。
3.2.1 return / child / sibling / index:链表四件套
js
this.return = null; // ReactFiber.js:149 父节点(概念上等同栈帧返回地址)
this.child = null; // ReactFiber.js:150 第一个子节点
this.sibling = null; // ReactFiber.js:151 下一个兄弟节点
this.index = 0; // ReactFiber.js:152 在兄弟列表中的位置
这是 Fiber 树之所以是"链表树"而非"嵌套对象树"的根本。看源码里 return 的注释(ReactInternalTypes.js:122):
The Fiber to return to after finishing processing this one. This is effectively the parent, but there can be multiple parents (two) so this is only the parent of the thing we're currently processing. It is conceptually the same as the return address of a stack frame.
它把 return 直接类比为"栈帧的返回地址"------这正是 Fiber 用循环模拟递归的核心:原本递归靠调用栈记住"处理完这个子节点后回到哪",现在改成显式用 return 指针记住。
index 的作用:当父节点协调出多个子节点时,每个子节点的 index 标记它在兄弟序列里的下标,方便在 ReactChildFiber.js 的协调逻辑里做"同位置复用"判断。createWorkInProgress 会把 index 从 current 拷贝过来(ReactFiber.js:411)。
3.2.2 ref / refCleanup
js
this.ref = null; // ReactFiber.js:154
this.refCleanup = null; // ReactFiber.js:155
ref 是用户通过 ref 属性传进来的引用(函数 ref 或 RefObject)。refCleanup 是 React 19 新增的"ref 清理函数"(配合 useRef 的清理语义)。在 createWorkInProgress 里这两个字段也会从 current 拷贝(ReactFiber.js:412-413),因为 ref 指向的实例没变。
3.2.3 pendingProps / memoizedProps
js
this.pendingProps = pendingProps; // ReactFiber.js:157 本次渲染"收到的"新 props
this.memoizedProps = null; // ReactFiber.js:158 上一次渲染"用过的" props
pendingProps:来自 React 元素的新 props,是"输入"。memoizedProps:上一次完成渲染时记住的 props,是"输出快照"。
memoizedProps 是 bailout(提前退出优化)的关键判据之一 :如果 pendingProps === memoizedProps(或经浅比较相等),且其它条件也满足,React 就可以跳过这个子树的工作(bailout)。在 performUnitOfWork 末尾(ReactFiberWorkLoop.js:3116)会执行 unitOfWork.memoizedProps = unitOfWork.pendingProps,即"渲染完成后,把输入变成记忆值"。
3.2.4 updateQueue / memoizedState
js
this.updateQueue = null; // ReactFiber.js:159 状态更新队列(setState/useState 等)
this.memoizedState = null; // ReactFiber.js:160 当前已生效的状态快照
- 类组件:
memoizedState是this.state,updateQueue是类组件的更新队列。 - 函数组件:
memoizedState是 Hooks 链表的首节点 (每个 Hook 的memoizedState串成链表),updateQueue是 Effect 链表(第 7 章 Hooks 会展开)。
这两个字段在 createWorkInProgress 里被直接共享引用而非深拷贝(ReactFiber.js:389-390),因为渲染期间要基于"上一棵树的状态"推导出"这一棵树的状态",共享能避免大量分配。
3.2.5 dependencies
js
this.dependencies = null; // ReactFiber.js:161
存储该 Fiber 订阅的 Context / 外部事件等依赖。注意它在 createWorkInProgress 里被克隆而非共享(ReactFiber.js:394-407),因为渲染阶段会修改它(标记哪些 context 被消费、哪些 lane 被依赖),不能和 current 共享同一份。
3.2.6 mode: TypeOfMode
js
this.mode = mode; // ReactFiber.js:163
mode 是一个位掩码 (bitmask),描述子树的渲染特性,例如 ConcurrentMode、StrictMode、ProfileMode、StrictEffectsMode 等。源码注释(ReactInternalTypes.js:155)强调:子节点在创建时继承父节点的 mode 。也就是说,createHostRootFiber 决定根的 mode,所有后代 Fiber 在 createFiberFrom* 时把父节点的 mode 传下去(createFiber(fiberTag, pendingProps, key, mode) 的最后一个参数就是父节点的 mode)。
mode 在渲染时会被频繁检查,例如 performWorkOnRoot 里 checkIfRootIsPrerendering、StrictMode 下的双调用、Profiler 计时等,全靠它。
3.2.7 flags / subtreeFlags / deletions:副作用三件套
js
this.flags = NoFlags; // ReactFiber.js:166 本节点自身的副作用标记
this.subtreeFlags = NoFlags; // ReactFiber.js:167 子树(含自身)汇总到本节点的副作用
this.deletions = null; // ReactFiber.js:168 本节点下需要删除的子 Fiber 数组
这是 Fiber 架构里"先渲染、后提交 "的枢纽。渲染阶段(beginWork / completeWork)只是给 Fiber 打上 flags 标记(例如 Placement 表示要插入 DOM、Update 表示要更新、Ref 表示要处理 ref);subtreeFlags 则在 completeWork 里由子节点向上"冒泡"汇总(bubbleProperties,ReactFiberCompleteWork.js:793)。真正执行 DOM 操作是在之后的 Commit 阶段(第 6 章 DOM)。
deletions 是个数组而非单个,因为一次提交可能删除多个子节点。关于这些 flag 的完整定义与分组,本章第五节专门拆解。
3.2.8 lanes / childLanes:车道双字段
js
this.lanes = NoLanes; // ReactFiber.js:170 本节点上"有待处理工作"的车道集合
this.childLanes = NoLanes; // ReactFiber.js:171 子树中"有待处理工作"的车道集合
lanes:这个 Fiber 自己挂了哪些优先级的更新。childLanes:它的整棵子树累计了哪些优先级的工作(用于快速判断"这个子树里是否还有某优先级的活要干")。
childLanes 的汇总同样发生在 bubbleProperties(ReactFiberCompleteWork.js:811-814)里:每个完成的节点把自己和所有子节点的 lanes / childLanes 合并,向上冒泡。这是 Lane 调度能高效"跳过无关子树"的基础。
3.2.9 alternate:双缓冲的灵魂
js
this.alternate = null; // ReactFiber.js:173
alternate 指向"另一个版本的我"------current 树的节点,其 alternate 指向 workInProgress 树里对应的节点,反之亦然。它把两棵树互相勾连成一对"克隆替身"。整章第二节和第四节会围绕它展开,这里先记住:它是双缓冲能否成立的关键字段。
3.3 Profiler 与 DEV-only 字段
enableProfilerTimer 开启时(通常由 DEV 构建或 Profiler 包裹触发),Fiber 上会增加四个计时字段(ReactFiber.js:189-192):
js
this.actualDuration = -0; // 本次渲染本节点及其子树实际耗时
this.actualStartTime = -1.1; // 渲染开始时间
this.selfBaseDuration = -0; // 本节点自身基础耗时
this.treeBaseDuration = -0; // 所有后代基础耗时之和
源码注释特别解释了一处反直觉的写法(ReactFiber.js:175-187):这些字段被故意初始化为 double 类型 (如 -0、-1.1),而不是普通的 0/1。原因是 v8 引擎的一个性能陷阱------如果把字段先初始化成 SMI(小整数)再赋成 double,会导致 Fiber 对象"改变 shape(隐藏类)",使对象无法被 v8 优化成稳定结构,进而让 React 在 DEV 下"unusably slow"。用 double 初始化可避免这个性能悬崖(https://github.com/facebook/react/issues/14365)。
在 DEV 构建下,Fiber 还会挂一系列 _debug* 字段(ReactFiber.js:197-202):_debugInfo、_debugOwner、_debugStack、_debugTask、_debugNeedsRemount、_debugHookTypes,用于 DevTools 和错误栈。最重要的是 ReactFiber.js:203-205 的冻结:
js
if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
Object.preventExtensions(this);
}
开发模式下每个 Fiber 都会被 Object.preventExtensions 冻结 ,防止运行时代码意外给 Fiber 加属性(一旦加了就会破坏前面的"固定 shape"优化,并且让 DevTools 等工具假设失效)。注意这个冻结在 enableObjectFiber 的对象字面量分支里同样存在(ReactFiber.js:292-294)。前面 ReactFiber.js:118-132 还有一个 hasBadMapPolyfill 的探测:某些坏的 Map polyfill 无法把不可扩展对象作为 key,此时就不冻结。
3.4 createFiber 的两个实现与 enableObjectFiber
createFiber 在 ReactFiber.js:299 被定义为二选一:
js
const createFiber = enableObjectFiber
? createFiberImplObject
: createFiberImplClass;
createFiberImplClass(ReactFiber.js:222)用new FiberNode(...)构造,字段在构造函数里逐个赋值。createFiberImplObject(ReactFiber.js:232)用对象字面量,并且把tag/key/pendingProps/mode这几个"动态属性"放到对象末尾 (ReactFiber.js:270 注释dynamic properties at the end for more efficient hermes bytecode)。
源码注释(ReactFiber.js:209-221)还列了 5 条关于 Fiber 的"铁律":① 不要往 Fiber 上加实例方法(难以被 JIT 内联);② 不要用 instanceof Fiber 做类型判断(永远靠 tag);③ 未来可能用数字 key 以便非 JIT 环境优化;④ 可以轻松从构造函数切换成对象字面量(为了更快);⑤ 可以容易移植成 C struct。这些看似零散的注释,其实都在强调一个核心:Fiber 是贴近底层的"值类型"数据结构,要可被高效创建、复用、移植。
四、核心数据结构(二):FiberRoot 与双缓冲
讲完单个 Fiber,必须讲清"根"------FiberRoot 和 HostRoot 这俩常被人混淆的概念,以及把它们黏合在一起的 current 指针与 alternate 替身。
4.1 FiberRoot 与 HostRoot 的区别(极其重要)
初学者最常犯的错误是把"Fiber 树的根"和"React 应用的根"混为一谈。源码里其实是两个不同对象:
FiberRoot(定义在ReactFiberRoot.js:50的FiberRootNode,类型定义在ReactInternalTypes.js:212):它是"React 应用的根容器",一个 React 应用只有一个FiberRoot。它持有整个应用的全局调度状态:pendingLanes、suspendedLanes、expiredLanes、callbackNode(Scheduler 的回调句柄)、current(指向当前 Fiber 树的根 Fiber)、containerInfo(真实 DOM 容器)等等。HostRootFiber (就是tag === HostRoot的那个 Fiber,由createHostRootFiber创建):它是"Fiber 树的根节点",每个FiberRoot恰好有一个 HostRoot Fiber,且FiberRoot.current指向它。
两者的钩稽关系在 createFiberRoot(ReactFiberRoot.js:210-212)里建立:
js
const uninitializedFiber = createHostRootFiber(tag, isStrictMode);
root.current = uninitializedFiber; // FiberRoot 指向 Fiber 树根
uninitializedFiber.stateNode = root; // Fiber 树根反向指向 FiberRoot
这是一个循环引用 :FiberRoot.current → HostRoot Fiber,HostRoot Fiber.stateNode → FiberRoot。于是从任意 Fiber 沿 return 上行到顶,再读 .stateNode,就能拿到 FiberRoot,进而读到所有全局调度字段。这是整个调度系统能"从任意工作单元定位到根"的底层通道。
4.2 FiberRoot 的关键字段
FiberRootNode 构造函数(ReactFiberRoot.js:50-155)初始化了大量字段,挑和本章最相关的列:
js
this.tag = disableLegacyMode ? ConcurrentRoot : tag; // 41 行:LegacyRoot(0) 或 ConcurrentRoot(1)
this.current = null; // 66 行:当前已提交的 Fiber 树根
this.pendingLanes = NoLanes;// 77 行:所有待处理车道集合
this.suspendedLanes = NoLanes; // 78 行:因 Suspense 挂起的车道
this.pingedLanes = NoLanes; // 79 行:被 ping(数据就绪)唤醒的车道
this.expiredLanes = NoLanes; // 81 行:已过期(需强制同步)的车道
this.entangledLanes = NoLanes; // 88 行:相互纠缠的车道
this.entanglements = createLaneMap(NoLanes); // 89 行:纠缠关系表
this.expirationTimes = createLaneMap(NoTimestamp); // 75 行:每条车道的过期时间戳
this.callbackNode = null; // 73 行:Scheduler 返回的回调句柄
this.callbackPriority = NoLane; // 74 行:当前调度优先级
注意 expirationTimes 和 entanglements 都用 createLaneMap 创建(ReactFiberLane.js:815),即长度为 31 的数组,下标对应车道位索引。这正是 Lane 模型"用数组按位索引"的体现,第六节会用到。
RootTag(ReactRootTags.js:10-13)只有两个值:
js
export type RootTag = 0 | 1;
export const LegacyRoot = 0; // 旧 API ReactDOM.render 用的
export const ConcurrentRoot = 1; // 新 API createRoot 用的
4.3 createHostRootFiber:根 Fiber 的 mode 从哪来
createHostRootFiber(ReactFiber.js:529-551)决定根 Fiber 的 mode:
js
export function createHostRootFiber(tag: RootTag, isStrictMode: boolean): Fiber {
let mode: number;
if (disableLegacyMode || tag === ConcurrentRoot) {
mode = ConcurrentMode;
if (isStrictMode === true) {
mode |= StrictLegacyMode | StrictEffectsMode;
}
} else {
mode = NoMode;
}
if (__DEV__ || (enableProfilerTimer && isDevToolsPresent)) {
mode |= ProfileMode;
}
return createFiber(HostRoot, null, null, mode);
}
要点:
- 只要不是 legacy 根(或全局关掉了 legacy 模式),根就是
ConcurrentMode。这正是createRoot默认开启并发能力、而老的ReactDOM.render是NoMode(同步)的源码依据。 StrictMode在创建根时就以StrictLegacyMode | StrictEffectsMode的形式合并进mode,并且由于子节点继承父mode,整个应用子树都会带上 StrictMode 语义(例如开发期双调用渲染、双调用 effect)。- DEV 或 Profiler 存在时强制加
ProfileMode,开启计时字段。
4.4 双缓冲(Double Buffering)的本质
所谓"双缓冲",借用图形学的概念:屏幕绘制时后台缓冲先画好,画完一次性翻转到前台,避免"画到一半就被看到"的闪烁。React 的 Fiber 树同理:
current树:用户当前看到的、已经提交(committed)的那棵树。它是"前台缓冲",永远保持完整、可用。workInProgress(WIP)树:后台正在构建、尚未提交的新树。它是"后台缓冲"。
两棵树的对应节点通过 alternate 互指:
为什么必须双缓冲? 因为 React 的渲染是可中断、可并发、可丢弃的:
- 如果直接在 current 树上改,渲染到一半被打断(比如来了一个更高优先级的输入更新),用户就会看到"半成品" UI。
- 双缓冲让"构建新树"和"展示旧树"彻底解耦:新树在后台慢慢构建、可以被打断、可以被丢弃(dispose 掉 WIP 树),直到它完整且一致 ,才通过
root.current = finishedWork(提交阶段切换指针)一次性翻转到前台。
提交阶段的指针切换就一行(概念上,在 ReactFiberRootScheduler 的 commit 完成处):把 FiberRoot.current 指向刚构建完成的 WIP 树根,同时 WIP 树根的 alternate 变成新的"下一棵后台树"。这一刻之后,原本的 current 树退居后台,成为下一次渲染的 WIP 替身------两棵树在内存里始终成对存在,交替扮演前台/后台。
4.5 createWorkInProgress:双缓冲的引擎(逐行)
这是全章最关键的函数之一(ReactFiber.js:323-437)。它回答一个问题:"我要开始处理 current 这个节点了,它的 WIP 替身在哪?" 我们逐行读:
js
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
// 分支 A:还没有替身,新建一个
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode; // 共享实例/DOM
if (__DEV__) {
workInProgress._debugOwner = current._debugOwner;
workInProgress._debugStack = current._debugStack;
workInProgress._debugTask = current._debugTask;
workInProgress._debugHookTypes = current._debugHookTypes;
}
workInProgress.alternate = current; // 互指
current.alternate = workInProgress; // 互指
} else {
// 分支 B:已有替身,复用之
workInProgress.pendingProps = pendingProps;
workInProgress.type = current.type;
workInProgress.flags = NoFlags;
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
// ...(见下)
}
这段源码自带的注释把双缓冲讲得明明白白(ReactFiber.js:326-330):
We use a double buffering pooling technique because we know that we'll only ever need at most two versions of a tree. We pool the "other" unused node that we're free to reuse. This is lazily created to avoid allocating extra objects for things that are never updated.
关键点:
- 懒创建(Lazily created) :只有当某节点真的要被更新时,才在分支 A 创建替身;从未被更新过的子树,其节点永远不会分配 WIP 节点------这就是注释说的 "avoid allocating extra objects for things that are never updated"。这直接回答了"双缓冲会不会让内存翻倍"的担忧:只有被触碰的子树才翻倍,静止的子树零成本。
- 分支 A 只拷贝"实例字段" :
elementType/type/stateNode直接共享引用。因为实例(DOM 节点、类实例)是同一个,没必要复制。
再看分支 B(ReactFiber.js:352-437)的"复用"逻辑,这是理解"WIP 树如何基于 current 树增量构建"的核心:
js
} else {
workInProgress.pendingProps = pendingProps;
workInProgress.type = current.type;
// 已有替身,重置 effect 标记
workInProgress.flags = NoFlags;
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
if (enableOptimisticKey) {
workInProgress.key = current.key;
}
if (enableProfilerTimer) {
workInProgress.actualDuration = -0;
workInProgress.actualStartTime = -1.1;
}
}
// 关键:保留静态 flag,重置动态 flag
workInProgress.flags = current.flags & StaticMask; // ReactFiber.js:383
workInProgress.childLanes = current.childLanes; // 384
workInProgress.lanes = current.lanes; // 385
// 直接共享"状态"引用
workInProgress.child = current.child; // 387
workInProgress.memoizedProps = current.memoizedProps; // 388
workInProgress.memoizedState = current.memoizedState; // 389
workInProgress.updateQueue = current.updateQueue; // 390
// dependencies 必须克隆(渲染期会改)
const currentDependencies = current.dependencies;
workInProgress.dependencies = currentDependencies === null ? null
: __DEV__ ? { lanes: ..., firstContext: ..., _debugThenableState: ... }
: { lanes: ..., firstContext: ... };
// 这些会在父节点的协调过程中被覆盖
workInProgress.sibling = current.sibling; // 410
workInProgress.index = current.index; // 411
workInProgress.ref = current.ref; // 412
workInProgress.refCleanup = current.refCleanup; // 413
逐条拆解这段复用逻辑的设计意图:
flags = current.flags & StaticMask(ReactFiber.js:383) :只保留"静态 flag"(如PassiveStatic、LayoutStatic、PortalStatic等,见第五节StaticMask),把"本次渲染产生的动态 effect"(Placement/Update/Ref等)全部清零。因为替身上的动态 effect 要重新计算,而静态 flag 描述的是"该子树是否含有某类 effect"(如是否含 passive effect),跨渲染有效,不必重算。这既能复用信息,又能保证每次渲染从干净状态出发。childLanes/lanes直接拷贝(384-385):优先级信息跨渲染保留(它们描述的是"待办工作",不是"本次 effect")。child/memoizedProps/memoizedState/updateQueue共享引用 (387-390):因为 WIP 要基于 current 的"上一次结果"推导新结果,共享引用避免深拷贝。注意这是读 current 写 WIP 的模式------渲染期绝不该修改 current 上的这些字段。dependencies克隆(394-407):因为它在渲染期会被修改(标记消费的 context、新的 thenable 状态),绝不能和 current 共享同一对象。sibling/index/ref/refCleanup先拷后覆盖(410-413):源码注释明确说 "These will be overridden during the parent's reconciliation"------父节点协调子节点时会重新设置这些,这里只是先给个默认值占个位。mode不在分支 B 里重置 :因为 WIP 在创建时(分支 A)就拷贝了current.mode,而mode在 Fiber 生命周期内不变,所以分支 B 无需处理。
最后还有 DEV 下的 resolveTypeForHotReloading(ReactFiber.js:420-433)和 _debugInfo 拷贝,这些是热更新与 DevTools 相关,不影响主线逻辑。
4.6 resetWorkInProgress:第二遍渲染的重置
除了 createWorkInProgress,还有一个 resetWorkInProgress(ReactFiber.js:440-527),用于"复用某个 Fiber 做第二遍渲染"的场景(例如 Suspense 解开后重放)。它和 createWorkInProgress 的区别在于:它不假定有 current 替身 ,而是把 Fiber 重置成"仿佛刚被 createFiber 创建"的状态(或"仿佛刚被 createWorkInProgress 克隆"的状态),并保留 actualTime 累计(ReactFiber.js:474-479 注释)。这个函数主要在 replay / 错误恢复路径使用,普通渲染走不到,理解双缓冲主线知道它存在即可。
4.7 各类 createFiberFrom* 工厂
协调阶段遇到不同元素类型时,会调用对应的 createFiberFrom* 工厂创建新 Fiber(不是替身,是首次创建的 current 节点)。这些工厂都最终调用 createFiber(tag, pendingProps, key, mode),区别只在 tag 和附带的 stateNode。举几个典型:
createFiberFromElement(ReactFiber.js:739-765):从 React 元素创建,内部转调createFiberFromTypeAndProps。createFiberFromTypeAndProps(ReactFiber.js:554-737):根据type判断该用哪个tag------函数 →FunctionComponent;类(shouldConstruct判断)→ClassComponent;字符串 →HostComponent(或 resource/singleton 变体);React.Fragment→Fragment并给mode加StrictLegacyMode;React.Suspense→SuspenseComponent;REACT_CONTEXT_TYPE→ContextProvider;REACT_FORWARD_REF_TYPE→ForwardRef;REACT_MEMO_TYPE→MemoComponent;无效类型 →Throw(创建一个会在 begin 阶段抛错的虚拟子节点)。createFiberFromText(ReactFiber.js:923):文本节点 →HostText。createFiberFromPortal(ReactFiber.js:941):Portal →HostPortal,且其stateNode是一个{containerInfo, pendingChildren, implementation}对象。
这些工厂创建的 Fiber,其 lanes 会被设为传入的 lanes(ReactFiber.js:730 fiber.lanes = lanes),把"这次创建属于哪个优先级"刻进节点,便于后续调度判断。
五、副作用标记(Flags)系统全解
渲染阶段不直接改 DOM,而是给 Fiber 打"副作用标记"。这些标记在 ReactFiberFlags.js 里定义,是31 位整数位掩码。我们把它分成"单个 flag"和"分组 mask"两部分讲。
5.1 单个 Flags 定义(含位布局)
源码里每个 flag 都用 0b... 字面量标明了位位置(ReactFiberFlags.js:18-96),我把它们整理成表,并标注优先级语义:
| Flag | 位值(十六进制等价) | 含义 |
|---|---|---|
NoFlags (18) |
0b0 |
无副作用 |
PerformedWork (19) |
0b1 |
本节点有工作发生(DevTools 用) |
Placement (20) |
0b10 |
需插入到 DOM |
DidCapture (21) |
0b10000000 |
Suspense 已捕获(配合 ShouldCapture) |
Hydrating (22) |
0b1000000000000 |
正在水合 |
Update (25) |
0b100 |
需更新 props/state |
Cloned (26) |
0b1000 |
被克隆 |
ChildDeletion (28) |
0b10000 |
有子节点待删除 |
ContentReset (29) |
0b100000 |
文本内容需重置 |
Callback (30) |
0b1000000 |
有 commit 回调(如 componentDidMount) |
ForceClientRender (33) |
0b100000000 |
强制客户端渲染(水合失败) |
Ref (34) |
0b1000000000 |
需处理 ref |
Snapshot (35) |
0b10000000000 |
需取快照(getSnapshotBeforeUpdate) |
Passive (36) |
0b100000000000 |
有 passive effect(useEffect) |
Visibility (39) |
0b1000000000000 |
可见性变化(Offscreen) |
StoreConsistency (40) |
0b10000000000000 |
外部 store 一致性 |
此外有一组 DEV-only 的高位 flag(ReactFiberFlags.js:94-96),用于 DevTools 追踪新建/挂载:
| Flag | 位值 | 含义 |
|---|---|---|
PlacementDEV (94) |
0b1000000000000000000000000000000 |
新建插入(DEV) |
MountLayoutDev (95) |
0b10000000000000000000000000000000 |
挂载布局 effect(DEV) |
MountPassiveDev (96) |
0b100000000000000000000000000000000 |
挂载 passive effect(DEV) |
以及一组静态 flag(ReactFiberFlags.js:71-91),它们描述"子树是否含某类 effect",跨渲染有效:
| 静态 Flag | 位值 | 含义 |
|---|---|---|
Forked (71) |
0b10000000000000000000000000000 |
分叉(Offscreen 分支) |
SnapshotStatic (72) |
0b100000000000000000000000000000 |
子树含快照 effect |
LayoutStatic/RefStatic (73) |
0b1000000000000000000000000000000 |
子树含布局/ref effect |
PassiveStatic (75) |
0b10000000000000000000000000000000 |
子树含 passive effect |
MaySuspendCommit (76) |
0b100000000000000000000000000000000 |
子树可能挂起提交 |
ViewTransitionStatic (85) |
0b10000000000000000000000000000000 |
ViewTransition 标记 |
ViewTransitionStaticParent (89) |
0b100000000000000000000000000000000 |
含 parentEnter 的 VT |
PortalStatic (91) |
0b1000000000000000000000000000000 |
子树含 Portal |
5.2 复用的 alias(互斥复用同一 bit)
源码注释(ReactFiberFlags.js:42-51)指出:因为 bit 快用完了,不同的 Fiber 类型之间可以复用同一个 bit,因为某些 flag 对不同类型互斥。例如:
js
export const Hydrate = Callback; // 45 行
export const ScheduleRetry = StoreConsistency; // 46 行
export const ShouldSuspendCommit = Visibility; // 47 行
export const ViewTransitionNamedMount = ShouldSuspendCommit; // 48 行
export const DidDefer = ContentReset; // 49 行
export const FormReset = Snapshot; // 50 行
export const AffectedParentLayout = ContentReset; // 51 行
这是 bit 资源紧张的务实做法:同一个 bit 在不同 Fiber 类型上表达不同语义,但绝不会同时需要。
5.3 分组 Mask:提交阶段的"路由表"
为什么需要 mask?因为 Commit 阶段分多个子阶段(BeforeMutation、Mutation、Layout、Passive),每个子阶段只关心某一类 effect。与其遍历整棵树逐个判断 flag,不如用 subtreeFlags & Mask 快速跳过"整棵不含该类 effect 的子树"。相关定义在 ReactFiberFlags.js:101-150:
js
export const BeforeMutationMask: number =
Snapshot |
(enableCreateEventHandleAPI ? Update | ChildDeletion | Visibility
: enableEffectEventMutationPhase ? 0 : Update); // 101-113
export const MutationMask =
Placement | Update | ChildDeletion | ContentReset |
Ref | Hydrating | Visibility | FormReset; // 120-128
export const LayoutMask = Update | Callback | Ref | Visibility; // 129
export const PassiveMask = Passive | Visibility | ChildDeletion; // 132
export const PassiveTransitionMask = PassiveMask | Update | Placement; // 136
export const StaticMask =
LayoutStatic | PassiveStatic | RefStatic | MaySuspendCommit |
ViewTransitionStatic | ViewTransitionStaticParent |
ViewTransitionNamedStatic | PortalStatic | Forked; // 141-150
逐个解释:
MutationMask:Mutation 子阶段(真正操作 DOM:插入/更新/删除节点、改 ref)关心的 flag 集合。Commit 的 Mutation 阶段遍历时,只对subtreeFlags & MutationMask非空的子树深入。LayoutMask:Layout 子阶段(useLayoutEffect、某些 ref 回调)关心的。PassiveMask:Passive 子阶段(useEffect,异步调度)关心的。注意它包含ChildDeletion------因为卸载时的清理 effect 也在 passive 阶段跑。BeforeMutationMask:在 DOM 变更之前跑(主要是Snapshot,即getSnapshotBeforeUpdate),以及createEventHandle需要的 blur 处理。StaticMask:前面讲过的"跨渲染有效的静态 flag 汇总",被createWorkInProgress用来flags = current.flags & StaticMask保留。HostEffectMask(ReactFiberFlags.js:57):0b...1111111111111,是所有"宿主 commit flag"的并集,用于unwindUnitOfWork里next.flags &= HostEffectMask(ReactFiberWorkLoop.js:3454)清除非宿主 effect。
LifecycleEffectMask(ReactFiberFlags.js:53-54)= Passive | Update | Callback | Ref | Snapshot | StoreConsistency,是"生命周期类 effect"的汇总,在类组件错误恢复等路径用于清除。
5.4 Flags 与双缓冲的配合
注意:在 createWorkInProgress 的分支 B 里,flags 被重置为 NoFlags 然后与 StaticMask 取并(ReactFiber.js:359 先 workInProgress.flags = NoFlags 再 383 workInProgress.flags = current.flags & StaticMask)。这意味着每次渲染,每个 WIP 节点的动态 effect 都被清空重算,而静态信息被保留。这是保证"渲染可重复、可丢弃"的关键:如果 WIP 树被丢弃(中断),它身上残留的 flag 不会污染下次------因为下次又会从 current 重新克隆。
六、Lane(车道)模型:31 位优先级位掩码
Lane 模型是 React 16.13 之后取代 expirationTime(时间戳)的优先级系统。它在 ReactFiberLane.js 里定义。本节逐常量、逐函数拆解,并回答"为什么位掩码比时间戳好"。
6.1 为什么用位掩码而不是时间戳
在 React 15/16 早期,优先级是用一个 31 位整数时间戳 expirationTime 表示的:越小越早过期,过期的必须同步执行。它有两个硬伤:
- 无法表达"多优先级并存":一个 Fiber 上如果有多个不同优先级的更新,时间戳模型只能用一个值近似,会丢失中间优先级。而位掩码天然支持"同时挂 16 个不同车道"。
- 无法精确表达"优先级区间" :例如"所有 transition 作为一个组可整体让出",时间戳做不到这种集合运算;位掩码用
& | ~即可。 - 数值比较不直观 :时间戳要算"当前时间 + 预算",且不同更新合并时语义模糊;位掩码里"数值越小优先级越高",
getHighestPriorityLane = lanes & -lanes一行拿到最高优先级,干净利落。
所以 React 改用 31 个车道(Lane),每个 Lane 是 31 位整数中的一个 bit 。一个 lanes 变量可以同时置多个 bit,表示"这批工作涉及这些优先级"。
6.2 Lane 常量与位布局
TotalLanes = 31(ReactFiberLane.js:41),即总共 31 条车道。我从源码(41-110 行)把布局整理成下表,位号从右(低位,优先级最高)到左(高位,优先级最低):
具体位值(ReactFiberLane.js:46-110,我摘关键几项):
SyncHydrationLane=0b1,SyncLane=0b10,InputContinuousHydrationLane=0b100,InputContinuousLane=0b1000,DefaultHydrationLane=0b10000,DefaultLane=0b100000,GestureLane=0b1000000。越靠右(bit 越小)优先级越高。SyncUpdateLanes = SyncLane | InputContinuousLane | DefaultLane(ReactFiberLane.js:56-57):这是"同步更新"的汇总,用于判断是否需要 flushSync。TransitionLanes(ReactFiberLane.js:62)=0b1111111111110000000(即 bit8 到 bit21,共 14 条 transition 车道)。它被进一步拆成TransitionUpdateLanes(前 10 条,ReactFiberLane.js:80)和TransitionDeferredLanes(后 4 条,ReactFiberLane.js:91)。为什么拆?因为 transition 内部也分"更新"和"延后"两组,分别用不同的过期时间(见 6.5)。RetryLanes(ReactFiberLane.js:94)=0b1111000000000000000000000000(4 条),用于 Suspense 重试。IdleLane/OffscreenLane/DeferredLane:最低优先级,用于空闲工作和离屏内容。
注意 TransitionLanes 是一段连续的 14 个 bit ,这是有意为之------让"所有 transition 可被整体识别(lane & TransitionLanes)",也能被"整体让出"。
6.3 基础位运算函数
这些函数是 Lane 系统的"算术单元"(ReactFiberLane.js:756-823):
js
// 取最高(最紧急)优先级的一条车道
export function getHighestPriorityLane(lanes: Lanes): Lane {
return lanes & -lanes; // 756-758
}
// 合并车道(集合并)
export function mergeLanes(a, b): Lanes {
return a | b; // 792-794
}
// 移除子集
export function removeLanes(set, subset): Lanes {
return set & ~subset; // 796-798
}
// 取交集
export function intersectLanes(a, b): Lanes {
return a & b; // 800-802
}
// 判断 a 是否包含 b 的任意位
export function includesSomeLane(a, b): boolean {
return (a & b) !== NoLanes; // 784-786
}
// 判断 subset 是否为 set 的子集
export function isSubsetOfLanes(set, subset): boolean {
return (set & subset) === subset; // 788-790
}
// 比较两条车道的优先级
export function higherPriorityLane(a, b): Lane {
return a !== NoLane && a < b ? a : b; // 810-813
}
// 取与 lanes 相等或更高优先级的所有位(掩码)
function getLanesOfEqualOrHigherPriority(lanes): Lanes {
const lowestPriorityLaneIndex = 31 - clz32(lanes);
return (1 << (lowestPriorityLaneIndex + 1)) - 1; // 760-766
}
getHighestPriorityLane 的 lanes & -lanes trick :-lanes 是 lanes 的补码(取反加一),它把 lanes 最低位的 1 保留、其余清零。例如 lanes = 0b10110,-lanes 的二进制最低位 1 在 bit2,lanes & -lanes = 0b10。这一行就能拿到"最紧急的那条车道",是整条调度链上调用最频繁的函数之一。
clz32 是"count leading zeros"(前导零个数),用 31 - clz32(lanes) 得到"最高有效位的下标",从而构造"从 bit0 到该位全 1"的掩码。
6.4 getNextLanes:选择下一批准渲染的车道(含中断保护)
getNextLanes(ReactFiberLane.js:249-361)是调度决策的"大脑":给定 root 和当前正在渲染的 wipLanes,它返回"接下来该渲染哪些车道"。我挑最关键的两段讲。
第一段是"基本选择"逻辑(ReactFiberLane.js:281-326):先分出非空闲车道 nonIdlePendingLanes,再按"未挂起 → 被 ping → 需预热"的顺序挑选;空闲车道同理。核心是它从不抢占"已接近完成"的工作------见中段(ReactFiberLane.js:337-358)的中断保护:
js
if (
wipLanes !== NoLanes &&
wipLanes !== nextLanes &&
(wipLanes & suspendedLanes) === NoLanes
) {
const nextLane = getHighestPriorityLane(nextLanes);
const wipLane = getHighestPriorityLane(wipLanes);
if (
nextLane >= wipLane ||
(nextLane === DefaultLane && (wipLane & TransitionLanes) !== NoLanes)
) {
// 保持渲染现有的 WIP 树,不要打断
return wipLanes;
}
}
这段极其关键,它实现了题述的**"DefaultLane 不抢占 TransitionLane"**规则:
- 如果新到的车道
nextLane优先级 低于或等于 正在渲染的wipLane(nextLane >= wipLane,注意数值越小优先级越高,所以"低优先级"用>=),不打断。 - 特别地,
nextLane === DefaultLane(默认优先级更新)且当前是TransitionLanes时,也不打断。因为 transition 和 default 在语义上接近,但 transition 不支持"刷新式 transition",所以允许 default 与 transition 在同一批渲染,避免无谓打断------源码注释(ReactFiberLane.js:350-353)明确写明了这个理由。
这就是"位掩码优于时间戳"的一个直接证据:用 lane & TransitionLanes 这种集合判断,可以写出"一整组车道共享某种调度策略"的规则,而时间戳模型只能逐条比较。
6.5 饥饿(Starvation)与过期(Expiration)
并发调度有一个经典难题:如果不断有高优先级更新涌入,低优先级工作会不会永远得不到执行(饥饿)?React 用过期时间解决。两个核心函数:
6.5.1 computeExpirationTime(ReactFiberLane.js:477-539)
根据 lane 的类型给出"过期时间增量":
js
function computeExpirationTime(lane, currentTime) {
switch (lane) {
case SyncHydrationLane: case SyncLane:
case InputContinuousHydrationLane: case InputContinuousLane:
case GestureLane:
return currentTime + syncLaneExpirationMs; // 交互类,稍快过期
case DefaultHydrationLane: case DefaultLane:
case TransitionHydrationLane:
case TransitionLane1 ... TransitionLane14:
return currentTime + transitionLaneExpirationMs; // transition 类,较晚过期
case RetryLane1...RetryLane4:
return enableRetryLaneExpiration
? currentTime + retryLaneExpirationMs : NoTimestamp;
case SelectiveHydrationLane: case IdleHydrationLane:
case IdleLane: case OffscreenLane: case DeferredLane:
return NoTimestamp; // 空闲及更低优先级永不主动过期
default: ...
}
}
要点:
- 交互类车道(Sync / InputContinuous / Gesture)过期最快,因为它们代表用户正在操作,必须尽快完成,否则用户感到卡顿。
- Transition 类车道过期较慢,因为 transition 本就是"可延迟的",允许它让位给动画等。
- Idle 及更低优先级
NoTimestamp(永不过期):它们本来就允许被无限推迟(比如后台预取),不强求完成。
6.5.2 markStarvedLanesAsExpired(ReactFiberLane.js:541-588)
每次 yield(让出主线程)都会调用它,遍历所有 pending lane,给它们计算/检查过期时间:
js
export function markStarvedLanesAsExpired(root, currentTime): void {
const pendingLanes = root.pendingLanes;
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
const expirationTimes = root.expirationTimes;
let lanes = enableRetryLaneExpiration
? pendingLanes
: pendingLanes & ~RetryLanes; // 排除 retry(总是时间切片)
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
const expirationTime = expirationTimes[index];
if (expirationTime === NoTimestamp) {
if ((lane & suspendedLanes) === NoLanes || (lane & pingedLanes) !== NoLanes) {
// 假定是 CPU 密集型,计算新的过期时间
expirationTimes[index] = computeExpirationTime(lane, currentTime);
}
} else if (expirationTime <= currentTime) {
root.expiredLanes |= lane; // 这条车道过期了!
}
lanes &= ~lane;
}
}
逻辑:对每个 pending lane,如果还没过期时间且"未挂起或被 ping",就按当前时间算一个过期时间;如果已有过期时间且 ≤ 当前时间,就把它加入 root.expiredLanes。一旦某车道进入 expiredLanes,后续决策就会强制同步渲染它 (见第七节 includesExpiredLane),防止饥饿。
注意它对 RetryLanes 的排除(ReactFiberLane.js:562-564):retry 车道总是时间切片的,不应被强制同步------因为 Suspense 重试可能触发大量重渲染,强制同步反而更卡。
6.6 claimNextTransitionUpdateLane:车道的循环分配
claimNextTransitionUpdateLane(ReactFiberLane.js:726-736)用"指针循环"的方式给每个新 transition 分配一条车道:
js
export function claimNextTransitionUpdateLane(): Lane {
const lane = nextTransitionUpdateLane;
nextTransitionUpdateLane <<= 1; // 指针左移一位
if ((nextTransitionUpdateLane & TransitionUpdateLanes) === NoLanes) {
nextTransitionUpdateLane = TransitionLane1; // 用完 10 条后回到开头
}
return lane;
}
配合 claimNextTransitionDeferredLane(738)和 claimNextRetryLane(747),React 用模块级变量 维护"下一条可用车道"的指针。因为 TransitionLanes 有 14 条,大多数情况下每个 transition 拿到独立车道(便于独立打断、独立过期),用完后循环复用。这就是为什么 startTransition 包起来的多个更新可能落在不同车道上、可以独立调度。
6.7 includesBlockingLane / includesExpiredLane:并发与否的判据
这两个函数直接被 performWorkOnRoot 的 shouldTimeSlice 使用(见第七节),所以在此先定义清楚:
js
export function includesBlockingLane(lanes): boolean { // 684-694
const SyncDefaultLanes =
SyncHydrationLane | SyncLane | InputContinuousHydrationLane |
InputContinuousLane | DefaultHydrationLane | DefaultLane | GestureLane;
return (lanes & SyncDefaultLanes) !== NoLanes;
}
export function includesExpiredLane(root, lanes): boolean { // 696-700
// 单独检查,因为车道可能在渲染开始后才过期
return (lanes & root.expiredLanes) !== NoLanes;
}
includesBlockingLane 把"同步 + 默认 + 手势"类都视为"阻塞型车道"------它们要么本来就要求同步,要么优先级足够高不该被时间切片。includesExpiredLane 检查是否触碰了已过期车道。
七、Work Loop(工作循环):渲染如何被切成可中断的单元
前面铺垫的 Fiber 节点、双缓冲、Lane,最终都要在 work loop 里被驱动起来。本节把渲染的"驱动引擎"逐函数拆开:从 scheduleUpdateOnFiber 到 ensureRootIsScheduled,从 performWorkOnRoot 的 shouldTimeSlice 判定到 renderRootSync / renderRootConcurrent,再到 performUnitOfWork 与 completeUnitOfWork,最后落到三个 work loop 变体。
7.1 入口:scheduleUpdateOnFiber(ReactFiberWorkLoop.js:987-1113)
当一次更新(setState / useState / 事件)触发时,最终会调到 scheduleUpdateOnFiber(root, fiber, lane)。它做的事:
- DEV 下检查是否在 insertion effect 里调度更新(报错)、是否在 passive 阶段调度(标记
didScheduleUpdateDuringPassiveEffects)。 - 检查当前是否正卡在 Suspense 的挂起中(ReactFiberWorkLoop.js:1006-1024):如果是,且新更新可能解开挂起,就
prepareFreshStack(root, NoLanes)重来。 markRootUpdated(root, lane):把 lane 加入root.pendingLanes(并可能加入indicatorLanes等)。- 判断是否在渲染期(
RenderContext):如果是,标记workInProgressRootRenderPhaseUpdatedLanes(render phase update,这是 React 支持"渲染期 setState"的机制);否则走正常路径,调用ensureRootIsScheduled(root)(ReactFiberWorkLoop.js:1093)安排调度。 - 特别地,legacy 模式下如果是
SyncLane且不在 batch,立即flushSyncWorkOnLegacyRootsOnly()同步刷掉(ReactFiberWorkLoop.js:1094-1111),这是老 API 的兼容行为。
7.2 ensureRootIsScheduled 在哪(澄清一处与任务描述的偏差)
任务描述里说 ensureRootIsScheduled 在 ReactFiberWorkLoop.js,但源码里它实际位于 ReactFiberRootScheduler.js:116(本系列以源码为准,特此更正)。它的职责(ReactFiberRootScheduler.js:116-152):
js
export function ensureRootIsScheduled(root: FiberRoot): void {
// 把 root 加入"已调度根"链表(firstScheduledRoot / lastScheduledRoot)
if (root === lastScheduledRoot || root.next !== null) {
// 快速路径:已经在链表里
} else {
if (lastScheduledRoot === null) {
firstScheduledRoot = lastScheduledRoot = root;
} else {
lastScheduledRoot.next = root;
lastScheduledRoot = root;
}
}
mightHavePendingSyncWork = true;
ensureScheduleIsScheduled(); // 安排一个 microtask 处理根调度表
// ...
}
它做两件事:① 把 root 放进"所有有待处理工作的根"的链表;② 确保有一个 microtask(ensureScheduleIsScheduled → scheduleImmediateRootScheduleTask)会在当前事件末尾处理这张表。真正的"为 root 选优先级、调用 Scheduler 注册任务"逻辑在 scheduleTaskForRootDuringMicrotask(同文件)里,它内部会调用 getNextLanes(见 6.4)决定渲染哪些车道,并最终通过 Scheduler 的 scheduleCallback 注册 performRootWork(也就是 performWorkOnRoot)到任务队列。
衔接点 :这里就是本章通往"第 5 章 Scheduler"的接口------React 自己不直接用 setTimeout 跑渲染,而是把 performWorkOnRoot 作为一个回调交给 Scheduler,由 Scheduler 根据任务优先级和帧空闲情况决定何时执行。callbackNode / callbackPriority(FiberRoot 上)就是 Scheduler 返回的句柄和对应优先级。
7.3 performWorkOnRoot 与 shouldTimeSlice 判定(ReactFiberWorkLoop.js:1137-1325)
performWorkOnRoot(root, lanes, forceSync) 是被 Scheduler 回调执行的"渲染根"入口。它最核心的一段是 shouldTimeSlice(ReactFiberWorkLoop.js:1168-1177):
js
const shouldTimeSlice =
(!forceSync &&
!includesBlockingLane(lanes) &&
!includesExpiredLane(root, lanes)) ||
// 预渲染时即使车道是同步的,也走并发 work loop,避免阻塞主线程
checkIfRootIsPrerendering(root, lanes);
let exitStatus: RootExitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes, true);
这正好对应题述规则,逐条翻译:
!forceSync:调用方没有强制同步(例如flushSync会传forceSync = true)。!includesBlockingLane(lanes):本次车道不含"阻塞型车道"(同步/默认/手势,见 6.7)。!includesExpiredLane(root, lanes):本次车道没有过期的------过期车道必须同步渲染以防饥饿(对应 6.5 的饥饿防护)。- 任一不满足 → 走
renderRootSync(一跑到底,不 yield)。 - 特例:
checkIfRootIsPrerendering为真(正在预渲染,例如 SSR 预取)时,即使车道是同步的也走并发 work loop,目的是让预渲染永不阻塞主线程。源码注释(ReactFiberWorkLoop.js:1172-1176)明确写了这个意图。
随后是 do...while 的大循环(ReactFiberWorkLoop.js:1185-1322)处理各种 exitStatus:
RootInProgress:渲染还没完(yield 了或挂起了),break 出去等下次被调度。- 渲染完成(非 InProgress):先检查"并发渲染期间是否被穿插的事件改动了外部 store 导致不一致"(
isRenderConsistentWithExternalStores,ReactFiberWorkLoop.js:1224),不一致就再同步渲染一遍(1238)。 - 错误处理:
RootErrored时尝试recoverFromConcurrentError(1248-1291),通常是水合失败时丢弃服务端响应转客户端渲染;RootFatalErrored则标记 root 为 suspended 并停止(1292-1308)。 - 最后
finishConcurrentRender(1313)根据最终状态决定提交或延迟提交(Suspense 延迟提交逻辑)。
7.4 prepareFreshStack:开始一轮新渲染(ReactFiberWorkLoop.js:2023)
prepareFreshStack(root, lanes) 在每次"重新开始构建 WIP 树"时被调用。它的核心动作(结合调用点)是:设置全局 workInProgressRoot = root、workInProgressRootRenderLanes = lanes,并 workInProgress = createWorkInProgress(root.current, null)------即以 current 树根为蓝本,创建(或复用)WIP 树根,作为本轮渲染的起点。它还会重置一系列"本次渲染"的全局状态(interleaved lanes、pinged lanes、transitions 等)。
这里要特别注意 createWorkInProgress(root.current, null):它正是第四节讲的"双缓冲引擎"------拿 current 根的替身作为 WIP 根,从此 work loop 沿着 WIP 树向下协调。
7.5 renderRootSync 与 renderRootConcurrent(两种渲染驱动)
两者结构高度相似(源码注释 ReactFiberWorkLoop.js:2620-2622 也承认它们越来越像),区别只在内部调哪个 work loop 变体。
renderRootSync(ReactFiberWorkLoop.js:2623-2768)流程:
- 设置
executionContext |= RenderContext,push 两个 dispatcher(pushDispatcher/pushAsyncDispatcher)。 - 若
workInProgressRoot !== root || lanes 变了,说明要重开栈,调prepareFreshStack(2653)。 outer: do { try { ... workLoopSync(); ... } } while(true)(2725-2731):跑同步 work loop,直到完成或遇异常。- 捕获
handleThrow处理 Suspense / 错误。 - 完成后若
workInProgress === null,清理workInProgressRoot = null,finishQueueingConcurrentUpdates()。
renderRootConcurrent(ReactFiberWorkLoop.js:2779-3053)几乎一样,但内部根据 enableThrottledScheduling 选择 workLoopConcurrent 或 workLoopConcurrentByScheduler(ReactFiberWorkLoop.js:3014-3018):
js
if (__DEV__ && ReactSharedInternals.actQueue !== null) {
workLoopSync(); // act 环境下不 yield
} else if (enableThrottledScheduling) {
workLoopConcurrent(includesNonIdleWork(lanes));
} else {
workLoopConcurrentByScheduler();
}
注意 act 环境下(测试)故意走 workLoopSync 不 yield------因为测试环境 host I/O 被 mock,shouldYield() 不可信,必须跑完才能断言结果。
7.6 performUnitOfWork:一个工作单元(ReactFiberWorkLoop.js:3081-3123)
这是 work loop 的"单次迭代"------处理一个 Fiber,决定向下还是向上:
js
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate; // 它的 current 替身
let next;
// ...(DEV / Profiler 分支都调 beginWork)
next = beginWork(current, unitOfWork, entangledRenderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps; // 渲染完成,输入变记忆值
if (next === null) {
// 没有生成子节点 → 该节点"完成",向上 complete
completeUnitOfWork(unitOfWork);
} else {
// 有子节点 → 继续向下处理子节点
workInProgress = next;
}
}
逻辑极其清晰:
- 调
beginWork(ReactFiberBeginWork.js)做"向下协调"------对 HostComponent/FunctionComponent/ClassComponent 等各自生成子 Fiber,返回第一个子节点 (child)或null。 - 若
beginWork返回next === null(没有子节点了,或该子树完成),则进入completeUnitOfWork向上回溯。 - 否则把
workInProgress指向next,继续下一轮循环。
beginWork 内部会调用 createWorkInProgress 为子节点创建替身(这就是为什么 WIP 树是"边遍历边构建"的,而不是预先全建好)。
7.7 completeUnitOfWork 与 bubbleProperties:向上回溯 + 冒泡(ReactFiberWorkLoop.js:3368 / ReactFiberCompleteWork.js:793)
completeUnitOfWork(ReactFiberWorkLoop.js:3368-3431)处理"一个节点已完成子树的协调,现在要完成它自身"的情况:
js
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
if ((completedWork.flags & Incomplete) !== NoFlags) {
// 该节点没完成(某子节点抛错/挂起)→ 改走 unwind 而非 complete
unwindUnitOfWork(completedWork, skipSiblings);
return;
}
const current = completedWork.alternate;
const returnFiber = completedWork.return;
// ...调 completeWork(处理 DOM 创建/属性计算,挂 flag)
next = completeWork(current, completedWork, entangledRenderLanes);
if (next !== null) {
workInProgress = next; // complete 阶段也可能生成新工作
return;
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber; // 还有兄弟 → 处理兄弟
return;
}
completedWork = returnFiber; // 没有兄弟 → 回到父节点
workInProgress = completedWork;
} while (completedWork !== null);
if (workInProgressRootExitStatus === RootInProgress) {
workInProgressRootExitStatus = RootCompleted; // 回到根 → 整棵树完成
}
}
遍历顺序是深度优先的后序 (child → sibling → return),这保证"子节点先于父节点完成",从而父节点能汇总子的 effect。completeWork(ReactFiberCompleteWork.js)负责具体"完成"动作(如 HostComponent 创建 DOM 实例、计算 DOM diff 所需的 flag),本章不展开其细节(第 6 章 DOM 会讲)。
bubbleProperties(ReactFiberCompleteWork.js:793-853)是 effect/车道冒泡 的核心,在每个节点 complete 时把它和所有子的 flags/subtreeFlags/childLanes 向上汇总:
js
function bubbleProperties(completedWork: Fiber) {
const didBailout =
completedWork.alternate !== null &&
completedWork.alternate.child === completedWork.child;
let newChildLanes = NoLanes;
let subtreeFlags = NoFlags;
if (!didBailout) {
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes)); // 汇总车道
subtreeFlags |= child.subtreeFlags; // 汇总子树 flag
subtreeFlags |= child.flags; // 汇总子自身 flag
child.return = completedWork; // 修正回指针
child = child.sibling;
}
completedWork.subtreeFlags = subtreeFlags; // 写回本节点 subtreeFlags
completedWork.childLanes = newChildLanes; // 写回本节点 childLanes
}
// ...(Profiler 模式下还累加 actualDuration / treeBaseDuration)
}
注意 didBailout 判断(ReactFiberCompleteWork.js:794-796):如果 WIP 节点的 child 和 current 替身的 child 是同一个对象(说明这个子树被 bailout、没有真正工作),就不冒泡(因为子树 flag 没变,不必重算)。这是 bailout 优化的一环。
冒泡的意义 :完成渲染后,每个节点的 subtreeFlags 都精确包含了"它整棵子树需要哪些 effect"。Commit 阶段据此可以用 if (subtreeFlags & MutationMask) ... 快速跳过"整棵不需要做 DOM 变更的树",复杂度从"遍历每个节点"降为"只看路径上有 flag 的节点",这正是双缓冲 + 冒泡 flag 带来的性能红利。
7.8 三个 work loop 变体
源码里有三个"真正循环干活"的函数,它们的区别是何时让出主线程:
具体代码:
js
// 变体 1:同步,绝不 yield(ReactFiberWorkLoop.js:2772-2777)
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// 变体 2:按时间片 yield(ReactFiberWorkLoop.js:3056-3070)
function workLoopConcurrent(nonIdle: boolean) {
if (workInProgress !== null) {
const yieldAfter = now() + (nonIdle ? 25 : 5); // 非空闲 25ms,空闲 5ms
do {
performUnitOfWork(workInProgress);
} while (workInProgress !== null && now() < yieldAfter);
}
}
// 变体 3:按 Scheduler 让出(ReactFiberWorkLoop.js:3073-3079)
function workLoopConcurrentByScheduler() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
workLoopSync:用于renderRootSync(过期/强制同步渲染),以及 act 环境下的并发渲染。它不 yield,一次性把整棵树跑完------这也是为什么"过期车道强制同步"能在时间上兜底,杜绝饥饿。workLoopConcurrent:按"绝对时间片" yield。源码注释(ReactFiberWorkLoop.js:3057-3062)解释了一个反直觉的设计------对于 Transition / Retry,故意"每隔一帧"才 yield 一次(25ms),目的不是为了降低 yield 开销,而是故意让出主线程给动画等高频工作,把动画帧率压到约 30fps,避免 transition 渲染把动画饿死。Idle 工作则每 5ms yield 一次保流畅。workLoopConcurrentByScheduler:不自己算时间,而是问 Scheduler 的shouldYield()------由 Scheduler 根据"当前帧剩余时间"决定是否让出。这是最"正统"的并发模式,把"何时让出"完全交给第 5 章要讲的 Scheduler。
三种变体统一的前提都是 while (workInProgress !== null):只要 workInProgress 指针不为空,就继续处理下一个单元 。当 performUnitOfWork / completeUnitOfWork 一路回溯到根、把 workInProgress 设为 null,循环结束,渲染完成。
7.9 Work Loop 的全局状态(理解中断/恢复的关键)
work loop 不是纯函数------它依赖一组模块级全局变量来保存"渲染进行到哪了"(ReactFiberWorkLoop.js:439-545 一带),例如:
workInProgressRoot:正在渲染哪个 root。workInProgress:当前正在处理哪个 Fiber(就是循环指针)。workInProgressRootRenderLanes:本次渲染的车道。workInProgressSuspendedReason/workInProgressThrownValue:当前是否因 Suspense 挂起及抛出的 thenable。entangledRenderLanes:本次渲染纠缠的车道集合。
这些全局变量之所以必要,正是因为 work loop 要可中断可恢复 :yield 出去时,这些变量记下了"下一次该从哪个 Fiber 接着干";被 Scheduler 再次回调时,renderRootConcurrent 发现 workInProgressRoot === root && lanes 没变,就不调 prepareFreshStack,而是直接从 workInProgress 继续(ReactFiberWorkLoop.js:2787)。这就是"渲染断点续作"的实现机制------比递归版本强太多:递归一旦让出,调用栈就丢了。
八、为什么是链表而非树递归?为什么是双缓冲?为什么用位掩码?
本节正面回答题述三个"为什么",把它们从源码证据上升到设计哲学。
8.1 为什么 Fiber 用链表而非对象嵌套递归
本质原因:要让"深度优先递归"变成"可中断的循环"。
递归(React 15 的 Stack Reconciler)用调用栈隐式保存"处理完子节点后回哪"。一旦要"中断并恢复",调用栈无法被暂停、序列化、迁移------JavaScript 引擎不允许你 yield 一个正在进行的递归调用栈。
Fiber 的 return/child/sibling 链表,把"控制流状态"显式数据化:
child/sibling给出"下一步往哪走"(向下/向右)。return给出"走完回哪"(向上)。workInProgress全局指针给出"当前在哪"。
于是 work loop 可以写成 while (workInProgress) { ... } 这样的循环,每次迭代处理一个单元,任意时刻都能 return 把控制权交还主线程,然后凭着 workInProgress 和那一串 return 指针无损恢复。源码里 return 的注释(ReactInternalTypes.js:122)直接说它就是"栈帧的返回地址",就是这个意思。
附带的好处:链表天然支持"增量构建"和"bailout" 。对象树如果嵌套递归,你没法"只构建变化的部分";链表则可以在协调时按需创建子节点,未变化的子树直接复用 current 的替身(见 createWorkInProgress 的共享引用),实现局部更新。
8.2 为什么用双缓冲
本质原因:让"可丢弃、可中断的后台构建"与"永远可用的前台页面"解耦。
没有双缓冲时,你只能直接改 current 树------但渲染是可中断的,改到一半被打断,用户就看到半成品。双缓冲用 alternate 把两棵树配对:
- 后台 WIP 树随便改、随便丢弃、随便重来,反正用户看不到。
- 只有 WIP 树"完整且一致"时,才
root.current = finishedWork一次性翻转指针。 - 翻转后,旧 current 树自动变成"下一轮的 WIP 替身池",内存不浪费。
而且"懒创建替身"(ReactFiber.js:326-330 注释)让静止子树零额外内存------只有被更新的子树才会 current.alternate 非 null。这是图形学双缓冲思想在 UI 渲染里的优雅移植。
8.3 为什么位掩码优先级比 expirationTime 好
前面 6.1 已从三个角度对比,这里再归纳成一句话:expirationTime 是"一个数值",只能表达单一优先级且合并语义模糊;Lane 是"31 个 bit 的集合",能并行表达多个优先级、能用 & | ~ 做集合运算、能写出"整组车道共享调度策略"的规则(如 DefaultLane 不抢占 TransitionLane) 。它还能用 getHighestPriorityLane = lanes & -lanes 一行取最高优先级,性能与可读性双优。时间模型只是作为"防止饥饿的兜底"(computeExpirationTime / markStarvedLanesAsExpired)叠加在 Lane 之上,而非主优先级载体------这是 React 调度演进的精华。
九、与其他模块的衔接(承上启下)
本章是地基,上层模块都建在它之上。明确衔接点,方便你顺藤摸瓜读后续章节。
9.1 → 第 5 章 Scheduler
performWorkOnRoot是被 Scheduler 回调的(通过ensureRootIsScheduled→scheduleTaskForRootDuringMicrotask→ Scheduler.scheduleCallback)。workLoopConcurrentByScheduler的shouldYield()直接来自 Scheduler(它根据帧 deadline 决定让出)。FiberRoot.callbackNode/callbackPriority是 Scheduler 的回调句柄与优先级。本章的 Lane 是"React 内部优先级",Scheduler 的优先级(Immediate/UserBlocking/Normal/Idle)是"宿主环境优先级",两者通过lanePriorityToSchedulerPriority之类映射衔接------第 5 章详述。
9.2 → 第 6 章 DOM(Commit / Mutation)
- 渲染阶段(本章 work loop)只负责给 Fiber 打
flags(Placement/Update/ChildDeletion等)和冒泡subtreeFlags。 - 真正的 DOM 增删改发生在 Commit 阶段 ,它依据
subtreeFlags & MutationMask等跳过无关子树。本章 5.3 的分组 mask 正是为 Commit 阶段准备的"路由表"。completeWork(ReactFiberCompleteWork.js)里 HostComponent 创建 DOM 实例也是衔接点。
9.3 → 第 7 章 Hooks
- 函数组件的
memoizedState和updateQueue(本章 3.2.4)实际上是 Hooks 链表的首节点和 Effect 链表。 beginWork里对 FunctionComponent 会调renderWithHooks,它读取/写入 Fiber 上的 Hook 链表------Hooks 的状态不放在stateNode(函数组件没有实例),而是藏在memoizedState这条链表上。所以本章讲清了"Hook 状态存在哪",第 7 章讲"它怎么被读写"。
9.4 → Suspense / Transition / Offscreen
includesBlockingLane/checkIfRootIsPrerendering/markStarvedLanesAsExpired这些调度决策,直接决定了 Suspense 的延迟提交、Transition 的可中断渲染、Offscreen 的离屏缓存行为。shouldTimeSlice的"预渲染不走同步"分支,正是 SSR / 预取(Offscreen 预渲染)能不阻塞主线程的底层保障。
十、边界场景与坑
10.1 渲染期 setState(Render Phase Update)
在 performUnitOfWork 调用的组件函数里再次 setState,会被 scheduleUpdateOnFiber 捕获并设置 workInProgressRootRenderPhaseUpdatedLanes(ReactFiberWorkLoop.js:1041)。React 支持这种"渲染期更新",但它必须是当前正在渲染的同一组件(Hooks 里的 dispatch 内部有判断),否则会警告。底层机制就是:本次渲染还没提交,再次调度会合并进当前 WIP 树,而不是开新树。
10.2 并发下的" tearing(撕裂)"与外部 store
performWorkOnRoot 在并发渲染完成后会检查 isRenderConsistentWithExternalStones(ReactFiberWorkLoop.js:1224):如果渲染期间被穿插的事件改了外部 store(如 Redux),可能导致"部分 Fiber 读到旧值、部分读到新值"的撕裂。此时 React 会再同步渲染一遍(1238)以阻塞进一步变更,保证一致性。这是并发渲染最微妙的边界之一。
10.3 StrictMode 双调用与 Object.preventExtensions
DEV 下 StrictMode 会让 beginWork/completeWork 被调用两次 (双调用检测副作用不纯)。由于 Fiber 被 preventExtensions 冻结(ReactFiber.js:204),双调用不会意外加字段;但双调用意味着"渲染函数被跑两次",因此 React 要求渲染必须是纯函数(不能有副作用),否则你会看到副作用执行两次。这解释了为什么 3.3 节那个"冻结"不是可有可无的装饰,而是并发/严格模式正确性的底座。
10.4 中断后 WIP 树被丢弃,替身怎么办
如果一个 WIP 树因为更高优先级更新而中断且不再续作(例如被彻底取代),它的替身节点会随根切换而被"留在原地"------但只要新渲染以 current 为蓝本重新 createWorkInProgress,旧替身会被复用或最终被 GC。React 不会显式逐个 delete,靠的是 alternate 互指形成的"最多两棵树"约束(ReactFiber.js:326 注释),内存上自洽。
10.5 错误边界(Error Boundary)与 Incomplete flag
当子树抛错(包括 Suspense 之外的普通错误),completeUnitOfWork 检测到 flags & Incomplete 会改走 unwindUnitOfWork(ReactFiberWorkLoop.js:3381)向上回溯到最近的错误边界,沿途把父节点标记 Incomplete、清空 subtreeFlags(3485-3487),最后由边界捕获并渲染 fallback。这正是 bubbleProperties 之外"反向清理 flag"的路径,保证错误子树不会带着脏 flag 进入 Commit。
十一、性能影响(时间 / 空间)
11.1 时间复杂度
- 渲染本身 :work loop 是 O(节点数) 的线性遍历,每个节点
beginWork+completeWork一次。bubbleProperties在 complete 时遍历子链表也是 O(子节点数),整体仍是 O(总节点数),无额外放大。 - 调度决策 :
getNextLanes是 O(车道数=31) 的常数时间;getHighestPriorityLane是 O(1) 的位运算。调度开销极小。 - Commit 阶段 :靠
subtreeFlags掩码跳过无关子树,实际 DOM 操作只发生在有 flag 的路径上------最坏 O(全部),但通常远小于全部。
11.2 空间复杂度与双缓冲的内存代价
- 每个 Fiber 是一个独立对象(或字面量),节点总数 = 组件实例数。静止子树只有 current 一份。
- 双缓冲的额外开销 仅限"被更新的子树" :这些子树的 WIP 替身会和 current 共存,直到提交翻转。最坏情况(整棵树都更新)确实接近 2 倍 Fiber 对象,但
createWorkInProgress的懒创建(3.5 节)和"共享实例字段(stateNode/memoizedState 等)"把实际增量压得很低------复制的是 Fiber 节点壳,共享的是大块状态。 enableObjectFiber的对象字面量实现(ReactFiber.js:232)把动态字段放末尾优化 Hermes 字节码;preventExtensions在 DEV 下牺牲极少量开销换取 shape 稳定(3.3 节)。
11.3 何时成为瓶颈
- 超大组件树整体强制同步渲染 (例如
flushSync包住整页更新、forceSync或大批量过期车道):work loop 不 yield,长任务阻塞主线程。规避方法:避免在事件回调里用flushSync,用startTransition把非紧急更新降级到 TransitionLane。 - 过度细碎的更新导致频繁调度 :每次
scheduleUpdateOnFiber都会把 root 加入调度表并安排 microtask。批量更新(React 18+ 默认自动批处理)能显著减少调度次数。
十二、用法与最佳实践(结合源码解释"为什么这样写才对")
12.1 用 startTransition 把非紧急更新降级
源码依据:TransitionLane 走并发 work loop、过期较慢、且 DefaultLane 不抢占它(6.4、6.5)。所以把"搜索输入触发的整页重渲染"用 startTransition 包起来,就能让它在用户敲键盘(InputContinuousLane,阻塞型)时主动让位,避免卡顿输入。反例 :把搜索重渲染直接 setState,落在 DefaultLane,虽不被 transition 抢占,但也不享受 transition 的可中断与延迟提交,长列表仍可能掉帧。
12.2 给列表项加稳定的 key
key 是 Fiber 复用判断的核心(3.1.2)。用数组下标做 key,在增删项时会导致 React 复用错误的 Fiber(及其 state/ref),出现状态错乱。用稳定唯一 id 作 key,React 才能正确通过 createWorkInProgress 共享替身、命中 bailout。
12.3 不要在渲染函数里写副作用
因为 StrictMode 双调用(10.3)+ 并发可中断(渲染可能跑一半被丢弃),渲染期的副作用会被执行多次或作用于被丢弃的树。只有 useMemo/useReducer 等纯计算得放在渲染期 ,DOM 副作用必须放到 useEffect(Passive)或 useLayoutEffect(Layout)------它们由 Commit 阶段按 flag 精确触发,且只跑一次(严格模式 dev 下刻意双跑以便检测)。
12.4 利用 memo / useMemo 制造 bailout 机会
bailout 依赖 memoizedProps 与 pendingProps 的相等判断(3.2.3)。React.memo、类组件 shouldComponentUpdate、useMemo 都是在"阻止子树进入 work loop",从而省下 beginWork/completeWork 的整棵子树成本。理解 bubbleProperties 的 didBailout(7.7)你会发现:bailout 还能顺带省掉冒泡,是性价比最高的优化。
十二A、createWorkInProgress 逐行解构与字段重置矩阵
第四节 4.5 已经从"双缓冲引擎"的视角讲了 createWorkInProgress 的设计意图,这里换一个视角:把它当作一张"字段搬运表"逐行拆开 ,弄清"哪些字段被新建、哪些被重置、哪些被共享、哪些先拷后被覆盖"。这一节会贴出 packages/react-reconciler/src/ReactFiber.js:323-437 的完整源码,然后在表里给出每个字段的命运。
先看完整的源函数(ReactFiber.js:323-437):
js
// This is used to create an alternate fiber to do work on.
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
// We use a double buffering pooling technique because we know that we'll
// only ever need at most two versions of a tree. We pool the "other" unused
// node that we're free to reuse. This is lazily created to avoid allocating
// extra objects for things that are never updated. It also allow us to
// reclaim the extra memory if needed.
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
if (__DEV__) {
// DEV-only fields
workInProgress._debugOwner = current._debugOwner;
workInProgress._debugStack = current._debugStack;
workInProgress._debugTask = current._debugTask;
workInProgress._debugHookTypes = current._debugHookTypes;
}
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps;
// Needed because Blocks store data on type.
workInProgress.type = current.type;
// We already have an alternate.
// Reset the effect tag.
workInProgress.flags = NoFlags;
// The effects are no longer valid.
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
if (enableOptimisticKey) {
// For optimistic keys, the Fibers can have different keys if one is optimistic
// and the other one is filled in.
workInProgress.key = current.key;
}
if (enableProfilerTimer) {
// We intentionally reset, rather than copy, actualDuration & actualStartTime.
// This prevents time from endlessly accumulating in new commits.
// This has the downside of resetting values for different priority renders,
// But works for yielding (the common case) and should support resuming.
workInProgress.actualDuration = -0;
workInProgress.actualStartTime = -1.1;
}
}
// Reset all effects except static ones.
// Static effects are not specific to a render.
workInProgress.flags = current.flags & StaticMask;
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
// Clone the dependencies object. This is mutated during the render phase, so
// it cannot be shared with the current fiber.
const currentDependencies = current.dependencies;
workInProgress.dependencies =
currentDependencies === null
? null
: __DEV__
? {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
_debugThenableState: currentDependencies._debugThenableState,
}
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
// These will be overridden during the parent's reconciliation
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
workInProgress.refCleanup = current.refCleanup;
if (enableProfilerTimer) {
workInProgress.selfBaseDuration = current.selfBaseDuration;
workInProgress.treeBaseDuration = current.treeBaseDuration;
}
if (__DEV__) {
workInProgress._debugInfo = current._debugInfo;
workInProgress._debugNeedsRemount = current._debugNeedsRemount;
switch (workInProgress.tag) {
case FunctionComponent:
case SimpleMemoComponent:
case MemoComponent:
case ClassComponent:
case ForwardRef:
workInProgress.type = resolveTypeForHotReloading(current.type);
break;
default:
break;
}
}
return workInProgress;
}
第一处易被标题党误导的地方 :任务清单里提到"assignFiberPropertiesInDEV",但在本版本(React 19.3.1)的真实源码里并不存在该函数 (grep -n "assignFiberPropertiesInDEV" ReactFiber.js 无任何命中)。DEV 下"批量拷贝 _debug* 字段"的逻辑,已经被内联进 createWorkInProgress 正文 (分支 A 的 341-348 行拷贝 owner/stack/task/hookTypes,函数尾部 420-434 行拷贝 _debugInfo/_debugNeedsRemount 并按 tag 做热更新类型解析)。本系列以源码为准:没有 assignFiberPropertiesInDEV,只有两段分散在 createWorkInProgress 里的内联 DEV 拷贝 。之所以历史上要单独抽一个函数,是因为早期所有"新建 vs 复用"分支都要做同一份 DEV 拷贝;现在分支 A/B 的字段差异已经足够大,抽出来反而多一次函数调用,索性内联。这个细节也解释了为什么 341 行那段拷贝只出现在分支 A(新建替身)------因为分支 B 的替身对象本身已经带着上一轮拷贝过的 DEV 字段,无需重复。
逐段拆解:
-
let workInProgress = current.alternate;(324 行):双缓冲的第一原则------先问"替身池里有没有现成的" 。alternate就是替身槽位。 -
分支 A(325-351 行,
workInProgress === null):第一次要为这个 current 节点建替身。注意createFiber(current.tag, pendingProps, current.key, current.mode)传进去的是 current 的 tag/key/mode (替身的"身份"必须和本尊一致),但pendingProps用的是本次渲染的新 props 。随后elementType/type/stateNode三个实例字段直接共享引用 (337-339 行)------DOM 节点、类实例是同一个对象,复制它们毫无意义。最后 350-351 行建立双向互指 :workInProgress.alternate = current且current.alternate = workInProgress。这一步极其重要------它让"替身关系"成为对称的、可在任意方向 O(1) 跳转的。 -
分支 B(352-379 行,已有替身):复用。这里要区分两类字段:
- 显式覆写 :
pendingProps(本次新 props)、type(注释"Needed because Blocks store data on type",Blocks 已废弃但拷贝保留)、flags/subtreeFlags/deletions(清空上一轮的 effect)、key(仅enableOptimisticKey下)、actualDuration/actualStartTime(Proflier 下故意重置为 -0/-1.1 而非拷贝,见 372-375 行注释:防止时间跨 commit 无休止累加)。 - 不动 :
mode、elementType、stateNode、tag、index等已经在上一轮建替身时确定,本轮不再处理。
- 显式覆写 :
-
统一段 (381-418 行,两个分支都会走到):这是最容易看漏的部分。很多人以为"分支 A 和分支 B 各自处理完就结束了",实际上无论走哪个分支,最后都会执行这段。它做的事:
- 383 行
workInProgress.flags = current.flags & StaticMask:注意这会覆盖分支 B 里刚写的NoFlags!也就是说 359 行那句workInProgress.flags = NoFlags其实是"先清零再被统一段重新赋成current.flags & StaticMask"的两步走------先清后取静态位,效果等价于"只保留静态 flag"。这个细节值得在调试时注意:读 359 行不能孤立理解。 - 384-385 行:
childLanes/lanes从 current 直接拷贝(优先级跨渲染保留)。 - 387-390 行:
child/memoizedProps/memoizedState/updateQueue共享引用。 - 394-407 行:
dependencies克隆一个新对象 (不是共享),因为渲染期会往里写lanes/firstContext。 - 410-413 行:
sibling/index/ref/refCleanup先拷贝,注释明说"会在父节点协调时被覆盖"。 - 415-418 行:Profiler 下
selfBaseDuration/treeBaseDuration拷贝(注意与分支 B 里重置 的actualDuration形成对比:base 是静态的、actual 是本次的)。
- 383 行
-
420-434 行:DEV 下再补
_debugInfo/_debugNeedsRemount,并对函数/类/forwardRef/memo 类组件做resolveTypeForHotReloading(热更新时type可能被替换成新函数)。
把上面的搬运关系整理成一张"字段重置矩阵",一眼看清每个字段的来路:
| 字段 | 分支 A(首次建替身) | 分支 B(复用替身) | 统一段(共同) | 语义归类 |
|---|---|---|---|---|
tag |
createFiber(current.tag,...) 带入 |
不变 | --- | 身份(不可变) |
key |
createFiber(..., current.key, ...) |
enableOptimisticKey 下 = current.key |
--- | 身份(列表 Diff) |
elementType |
= current.elementType |
不变 | --- | 实例(共享) |
type |
= current.type |
= current.type |
DEV 下 resolveTypeForHotReloading |
实例(共享) |
stateNode |
= current.stateNode |
不变 | --- | 实例(共享) |
mode |
createFiber(..., current.mode) |
不变 | --- | 子树特性(不可变) |
pendingProps |
createFiber(..., pendingProps, ...) |
= pendingProps |
--- | 输入 |
memoizedProps |
createFiber 初始 null |
不变 | = current.memoizedProps |
上一次结果(共享) |
memoizedState |
createFiber 初始 null |
不变 | = current.memoizedState |
上次状态(共享) |
updateQueue |
createFiber 初始 null |
不变 | = current.updateQueue |
上次队列(共享) |
dependencies |
createFiber 初始 null |
不变 | 克隆新对象 | 渲染期会改(隔离) |
child |
createFiber 初始 null |
不变 | = current.child |
复用子树入口 |
sibling |
createFiber 初始 null |
不变 | = current.sibling |
先拷后覆盖 |
index |
createFiber 初始 0 |
不变 | = current.index |
先拷后覆盖 |
ref / refCleanup |
createFiber 初始 null |
不变 | = current.ref / = current.refCleanup |
先拷后覆盖 |
flags |
createFiber 初始 NoFlags |
先置 NoFlags(359) |
= current.flags & StaticMask |
动态清、静态留 |
subtreeFlags |
createFiber 初始 NoFlags |
= NoFlags(362) |
(不覆写) | 每轮重算 |
deletions |
createFiber 初始 null |
= null(363) |
--- | 每轮重算 |
lanes |
createFiber 初始 NoLanes |
不变 | = current.lanes |
优先级保留 |
childLanes |
createFiber 初始 NoLanes |
不变 | = current.childLanes |
优先级保留 |
alternate |
双向互指(350-351) | 不变(已互指) | --- | 双缓冲链接 |
actualDuration/actualStartTime |
createFiber 初始 -0/-1.1 |
重置 -0/-1.1(376-377) |
--- | 本次计时 |
selfBaseDuration/treeBaseDuration |
createFiber 初始 -0 |
不变 | = current.*(416-417) |
静态计时 |
从矩阵能读出两条关键结论:
- "共享引用"集中在"上一次的结果类字段" (
memoizedProps/memoizedState/updateQueue/child)。它们代表 current 树"已经提交的事实",WIP 需要读它们来推导新结果,共享既省内存又省拷贝。 - "必须隔离/重算"集中在"本次渲染会产生的东西" (
dependencies、flags、subtreeFlags、deletions、actual*)。它们一旦被 WIP 修改,绝不能污染 current,否则双缓冲的"current 永远干净"前提就破了。
为什么未更新的子树不分配 WIP
矩阵里 alternate 那一行的语义,直接回答了"双缓冲会不会让内存翻倍"这个高频疑问。createWorkInProgress 只在当前节点被真正走到 时才被调用(由 beginWork 决定是否对子节点递归),例如函数组件 updateFunctionComponent 里会调 reconcileChildren,进而对每个子元素调 createFiberFromElement 或复用 createWorkInProgress。反过来,如果一个子树命中了 bailout (pendingProps === memoizedProps 且无新 lane),beginWork 根本不会再向它的子节点递归,于是那些孙节点的 createWorkInProgress 一次都不会被调用 ,它们的 alternate 保持 null。这就是 326-330 行注释"lazily created to avoid allocating extra objects for things that are never updated"的字面含义:静止子树的 WIP 是"零分配",而不是"分配了但不用"。
需要强调的是,WIP 树在内存里并不是一棵完整独立的树 。它是"current 树的影子"------只有被访问过的节点才有替身,未被访问的节点的 child 指针会直接指向 current 子树的节点 (因为统一段 387 行 workInProgress.child = current.child)。所以严格来说,WIP 树是"部分新建 + 部分引用 current"的一张稀疏补丁。这也解释了一个常见困惑:为什么 debugger 里看 WIP 树有时会看到"同一个 Fiber 对象出现在两棵树里"------那正是因为该节点没被更新,两棵树共享了同一个 current 节点。
resetWorkInProgress:第二遍渲染的"精准复位"
与 createWorkInProgress 并排的是 resetWorkInProgress(ReactFiber.js:440-527),它的用途是:复用同一个 Fiber 对象做第二遍渲染 (例如 Suspense 数据就绪后 replay 刚才挂起的组件)。它不能简单调用 createWorkInProgress,因为后者假定"要么没有替身、要么替身是干净可复用的",而 resetWorkInProgress 面对的是"一个已经被写过、带着第一遍渲染残留状态的 Fiber",需要把它"复位成仿佛刚被 createFiber 或 createWorkInProgress 处理过"的样子。
源码里最值得注意的两点:
js
// Reset the effect flags but keep any Placement tags, since that's something
// that child fiber is setting, not the reconciliation.
workInProgress.flags &= StaticMask | Placement;
// The effects are no longer valid.
const current = workInProgress.alternate;
if (current === null) {
- 保留
Placement(454 行):注意这里比createWorkInProgress的current.flags & StaticMask多保了一个Placement。原因是注释所言------Placement是子 Fiber 设置给父 Fiber 的 (表示"我这个子节点要插入"),属于"已经发生的协调结论",第二遍渲染不应把它抹掉,否则父节点就漏掉了"要插入子节点"这件事。而StaticMask里那批静态 flag 同理需要保留。 - 两条复位路线 (458-524 行):若
alternate === null(这个 Fiber 本身就是"新节点",没有 current 对照),就复位成createFiber的初始值(child = null、memoizedProps = null、stateNode = null......);若alternate !== null,就复位成"createWorkInProgress克隆后的值"(child = current.child、memoizedProps = current.memoizedProps......)。两条路线分别对应"这个 Fiber 第一次创建"与"这个 Fiber 是从 current 克隆来的"两种出身。 - Profiler 计时故意不复位
actualDuration/actualStartTime:474-479 行与 518-523 行的注释都写明"我们不复位 actualTime 累计值,它用于跨多个渲染 pass 累计实际耗时"------这正好是resetWorkInProgress存在的动因之一:同一份工作被重放两遍时,时间要累加而不是清零,否则 Suspense replay 会把重放前的工作时间丢掉。
到这里,"双缓冲引擎"的两个函数(createWorkInProgress + resetWorkInProgress)就完整了:前者负责"常规增量构建替身",后者负责"异常路径下的精准复位"。理解了字段重置矩阵,再读第四节 4.4 的双缓冲图、第七节 7.4 的 prepareFreshStack(它调用的正是 createWorkInProgress(root.current, null)),就会清楚整条链路上每一步到底动了哪些字段。
十二C、initializeUpdateQueue 与 update queue 的环形链表
createFiberRoot 的最后一句 initializeUpdateQueue(uninitializedFiber)(ReactFiberRoot.js:233)给根 Fiber 挂上了一个更新队列。这个队列的结构是理解"setState / useState 到底把更新存到哪、怎么处理"的起点。相关类型定义在 packages/react-reconciler/src/ReactFiberClassUpdateQueue.js:131-153:
js
export type Update<State> = {
lane: Lane,
tag: 0 | 1 | 2 | 3,
payload: any,
callback: (() => mixed) | null,
next: Update<State> | null,
};
export type SharedQueue<State> = {
pending: Update<State> | null,
lanes: Lanes,
hiddenCallbacks: Array<() => mixed> | null,
};
export type UpdateQueue<State> = {
baseState: State,
firstBaseUpdate: Update<State> | null,
lastBaseUpdate: Update<State> | null,
shared: SharedQueue<State>,
callbacks: Array<() => mixed> | null,
};
initializeUpdateQueue(ReactFiberClassUpdateQueue.js:176-189)本身极短:
js
export function initializeUpdateQueue<State>(fiber: Fiber): void {
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
lanes: NoLanes,
hiddenCallbacks: null,
},
callbacks: null,
};
fiber.updateQueue = queue;
}
注意 baseState: fiber.memoizedState------队列的初始基准状态就是 Fiber 当前的 memoizedState 。对 HostRoot 而言就是 initialState(含 element/isDehydrated/cache);对类组件而言就是 this.state 的初值。
关键设计:shared.pending 是"单向环形链表"
UpdateQueue 里有两套链表,这是最容易看晕的地方:
shared.pending:环形链表 ,只存"待处理(还没被算进 baseState)"的新更新。它的 head 指向最后一个 入队的 update(即 tail 的next指回 head 之前的第一个),所以叫"环形"。firstBaseUpdate→lastBaseUpdate:线性链表 ,存"这一轮无法处理、需要留到下一轮重算"的更新(以及它们的baseState)。
为什么 pending 要用环形 而不直接用一个 head/tail 双指针?看 enqueueUpdate(ReactFiberClassUpdateQueue.js:223-274)里"渲染期更新"这条分支的入队代码:
js
if (isUnsafeClassRenderPhaseUpdate(fiber)) {
// This is an unsafe render phase update. Add directly to the update
// queue so we can process it immediately during the current render.
const pending = sharedQueue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
源码注释只写了"This is the first update. Create a circular list."------单条 update 的 next 指回自己即构成环。之所以采用这种只改 pending 一个共享变量 的追加方式(update.next = pending.next; pending.next = update; pending = update),是因为它把"入队"压缩成对共享指针的单点写 ,不需要同时维护 head 与 tail 两个字段保持一致(常规路径的并发入队走 enqueueConcurrentClassUpdate,见 ReactFiberClassUpdateQueue.js:272,动机同理)。pending 恒指向最后一个入队的 update,而 pending.next 指回第一个,故称"环形"。想象这条链的形态:
ini
pending 指向最后一个元素,pending.next 指向第一个元素(首尾相连)
入队新 update u:
u.next = pending.next // 新元素接在"第一个"前面
pending.next = u // 旧尾指向新元素
pending = u // pending 移到新尾
只需三步、且只需要写 pending 这一个共享变量 (前两步写的是新元素自己的字段),没有"head 和 tail 需要同时一致"的窗口。这在"多处以 microtask 并发插入更新"的场景下(React 18+ 的 concurrent updates,见 ReactFiberConcurrentUpdates.js)至关重要------后来者只需读一次 pending 就能把新 update 挂上去,不会出现 head/tail 短暂不一致导致的丢更新。
处理队列时(processUpdateQueue),源码每次会先把环形链表**"剪开"**:记下 pending.next 作为 firstPendingUpdate、pending 本身作为 lastPendingUpdate,然后 lastPendingUpdate.next = null 把环破成线性链表再接上 firstBaseUpdate。这样处理完后,shared.pending 被清空,而已处理的更新被"固化"进 firstBaseUpdate/lastBaseUpdate 这条线性链。相关代码在 ReactFiberClassUpdateQueue.js:504-547 一带(firstBaseUpdate/lastBaseUpdate 与 shared.pending 的拼接)。
为什么需要 baseState 与"base 链"
UpdateQueue 里同时有 shared.pending(新更新)和 baseState/firstBaseUpdate/lastBaseUpdate(基线),是因为优先级调度 :一次 processUpdateQueue 只处理"本次渲染车道内"的更新,低于本次优先级的更新会被跳过并保留 在 base 链上,连同"跳过它们时的中间状态"一起记为新的 baseState。这样当低优先级渲染真正执行时,可以从 baseState 出发、把被跳过的更新重放一遍,得到正确结果。源码 660-674 行就是这套"跳过低优先级更新并重设 base"的逻辑:
js
queue.lastBaseUpdate = lastPendingUpdate;
...
queue.baseState = newBaseState as any as State;
queue.firstBaseUpdate = newFirstBaseUpdate;
queue.lastBaseUpdate = newLastBaseUpdate;
这也解释了 cloneUpdateQueue(ReactFiberClassUpdateQueue.js:191+)存在的必要性:因为 baseState/firstBaseUpdate/lastBaseUpdate 会被"处理队列"这一动作修改,而 WIP 的 updateQueue 在 createWorkInProgress 里是与 current 共享引用的 (ReactFiber.js:390)。如果不克隆,处理 WIP 的队列就会污染 current 的队列。所以类组件 updateClassInstance 会先 cloneUpdateQueue(current, workInProgress),把 shared 引用保持共享(shared 装的是环形 pending,必须两棵树看到同一份)、但 baseState/firstBaseUpdate/lastBaseUpdate 拷一份新的。这正好呼应第十二A 的字段矩阵里"共享的是上一次结果、隔离的是本次渲染会改写的东西"这一总原则。
十二K、ReactFiberStack.js 的历史与作用
packages/react-reconciler/src/ReactFiberStack.js 全文只有 67 行,却是一个容易被忽略的关键小工具:它提供了一套通用的"游标显式栈",被 reconciler 里几乎所有"进入节点时压入、离开节点时弹出"的上下文模块共用。
完整源码:
js
export type StackCursor<T> = {current: T};
const valueStack: Array<any> = [];
let fiberStack: Array<Fiber | null>;
if (__DEV__) {
fiberStack = [];
}
let index = -1;
function createCursor<T>(defaultValue: T): StackCursor<T> {
return {
current: defaultValue,
};
}
function pop<T>(cursor: StackCursor<T>, fiber: Fiber): void {
if (index < 0) {
if (__DEV__) {
console.error('Unexpected pop.');
}
return;
}
if (__DEV__) {
if (fiber !== fiberStack[index]) {
console.error('Unexpected Fiber popped.');
}
}
cursor.current = valueStack[index];
valueStack[index] = null;
if (__DEV__) {
fiberStack[index] = null;
}
index--;
}
function push<T>(cursor: StackCursor<T>, value: T, fiber: Fiber): void {
index++;
valueStack[index] = cursor.current;
if (__DEV__) {
fiberStack[index] = fiber;
}
cursor.current = value;
}
export {createCursor, pop, push};
它现在的作用 :实现"显式栈 ",用来替代那些"依赖调用栈"的隐式上下文(典型代表是旧版 Context 的 _currentValue、以及各种"进入节点时压入、离开时弹出"的标量状态)。用法是:
createCursor(defaultValue)造一个游标{current};push(cursor, value, fiber):把游标的旧值压进valueStack,并把cursor.current设为新值;pop(cursor, fiber):把valueStack顶部的旧值弹回cursor.current。
注意 push/pop 的第三个参数是 fiber,但生产环境根本不用它 ------它只在 __DEV__ 下被 fiberStack 记录,用来在 pop 时校验"弹出的 Fiber 是否与压入时同一个"(38-42 行 Unexpected Fiber popped.)。这是典型的"开发期断言、生产期零成本"设计:fiberStack 整个变量在非 DEV 下都不分配(18-20 行)。
为什么说是"历史遗留 + 简化" :valueStack 是一个模块级数组 + 一个模块级 index ,所有游标共用同一份栈空间。这意味着 push/pop 必须严格配对、且跨游标也要保持 LIFO 顺序 ------如果 A 游标 push 后 B 游标 push,却先 pop 了 A,栈就乱了。多个上下文模块都共享这套机制------ReactFiberHostContext.js:26、ReactFiberNewContext.js:22、ReactFiberSuspenseContext.js:16、ReactFiberHiddenContext.js、ReactFiberLegacyContext.js、ReactFiberTracingMarkerComponent.js、ReactFiberTransition.js 都 import {createCursor, push, pop} from './ReactFiberStack',各自 createCursor 出若干游标挂在同一份 valueStack 上。因此"所有游标的 push/pop 混合后仍必须整体保持 LIFO 配对"是这套实现的硬约束(也正因如此,push/pop 的 fiber 参数在 DEV 下要逐一校验弹栈顺序)。
resetWorkInProgressStack 与它的关系 :resetWorkInProgressStack(ReactFiberWorkLoop.js:1979-2002)并不直接操作 valueStack,它做的是"回溯 WIP 链、把每个已进入的 Fiber 弹出上下文栈":
js
function resetWorkInProgressStack() {
if (workInProgress === null) return;
let interruptedWork;
if (workInProgressSuspendedReason === NotSuspended) {
// Normal case. Work-in-progress hasn't started yet. Unwind all
// its parents.
interruptedWork = workInProgress.return;
} else {
// Work-in-progress is in suspended state. Reset the work loop and unwind
// both the suspended fiber and all its parents.
resetSuspendedWorkLoopOnUnwind(workInProgress);
interruptedWork = workInProgress;
}
while (interruptedWork !== null) {
const current = interruptedWork.alternate;
unwindInterruptedWork(
current,
interruptedWork,
workInProgressRootRenderLanes,
);
interruptedWork = interruptedWork.return;
}
workInProgress = null;
}
它被 prepareFreshStack(2254 行)以及"水合中断"路径(2686、2995 行)调用。核心是:当一轮渲染被彻底放弃、要从头开始新渲染时,必须把之前"进入过的 Fiber"逐个 unwind ,而 unwindInterruptedWork 内部会 popTreeContext/弹出各类上下文游标(即调用 ReactFiberStack 的 pop)。如果不做这一步,valueStack 会残留上一轮的压栈记录,导致新渲染读到错误的上下文值------这正是"显式栈必须与渲染进度严格同步"的体现。所以 ReactFiberStack.js 虽然小,却是"中断/重开渲染"能保持栈状态干净的关键拼图;它与双缓冲的关系是:双缓冲保证"数据(Fiber 树)"可以丢弃重来,ReactFiberStack 保证"上下文(游标栈)"也能跟着丢弃重来。
十三、源码索引表
| 主题 | 文件 | 函数 / 符号 | 行号 |
|---|---|---|---|
| Fiber 节点构造(构造函数) | ReactFiber.js |
FiberNode |
134-207 |
| Fiber 节点构造(对象字面量) | ReactFiber.js |
createFiberImplObject |
232-297 |
| 选择实现 | ReactFiber.js |
createFiber |
299-301 |
| 双缓冲引擎 | ReactFiber.js |
createWorkInProgress |
323-437 |
| 第二遍重置 | ReactFiber.js |
resetWorkInProgress |
440-527 |
| 根 Fiber 创建 | ReactFiber.js |
createHostRootFiber |
529-551 |
| 从元素创建 Fiber | ReactFiber.js |
createFiberFromTypeAndProps / createFiberFromElement |
554-737 / 739-765 |
| 文本/Portal 等工厂 | ReactFiber.js |
createFiberFromText / createFiberFromPortal 等 |
923-955 |
| Fiber 类型定义 | ReactInternalTypes.js |
Fiber |
89-210 |
| FiberRoot 类型定义 | ReactInternalTypes.js |
BaseFiberRootProperties / FiberRoot |
212-296 / 386 |
| FiberRoot 构造 | ReactFiberRoot.js |
FiberRootNode |
50-155 |
| FiberRoot 创建(循环引用) | ReactFiberRoot.js |
createFiberRoot |
157-236 |
| 根 Tag | ReactRootTags.js |
LegacyRoot / ConcurrentRoot |
10-13 |
| 上下文栈(begin/complete 用) | ReactFiberStack.js |
push / pop / createCursor |
24-66 |
| 所有 Flags 定义 | ReactFiberFlags.js |
各 export const |
18-96 |
| 分组 Mask | ReactFiberFlags.js |
MutationMask/LayoutMask/PassiveMask/StaticMask 等 |
101-150 |
| Lane 常量 | ReactFiberLane.js |
SyncLane/TransitionLanes/... |
41-123 |
| 取最高优先级车道 | ReactFiberLane.js |
getHighestPriorityLane |
756-758 |
| 合并车道 | ReactFiberLane.js |
mergeLanes |
792-794 |
| 选择下一车道(中断保护) | ReactFiberLane.js |
getNextLanes |
249-361 |
| 计算过期时间 | ReactFiberLane.js |
computeExpirationTime |
477-539 |
| 标记饥饿车道为过期 | ReactFiberLane.js |
markStarvedLanesAsExpired |
541-588 |
| 分配 transition 车道 | ReactFiberLane.js |
claimNextTransitionUpdateLane |
726-736 |
| 阻塞/过期判断 | ReactFiberLane.js |
includesBlockingLane / includesExpiredLane |
684-700 |
| 调度更新入口 | ReactFiberWorkLoop.js |
scheduleUpdateOnFiber |
987-1113 |
| 渲染根(shouldTimeSlice 判定) | ReactFiberWorkLoop.js |
performWorkOnRoot |
1137-1325 |
| 一致性检查后重渲染 | ReactFiberWorkLoop.js |
performWorkOnRoot(内部) |
1224-1243 |
| 准备新栈 | ReactFiberWorkLoop.js |
prepareFreshStack |
2023-2070+ |
| 同步渲染 | ReactFiberWorkLoop.js |
renderRootSync |
2623-2768 |
| 并发渲染 | ReactFiberWorkLoop.js |
renderRootConcurrent |
2779-3053 |
| 工作单元 | ReactFiberWorkLoop.js |
performUnitOfWork |
3081-3123 |
| 重放挂起单元 | ReactFiberWorkLoop.js |
replaySuspendedUnitOfWork |
3125-3142 |
| 同步 work loop | ReactFiberWorkLoop.js |
workLoopSync |
2772-2777 |
| 时间片 work loop | ReactFiberWorkLoop.js |
workLoopConcurrent |
3056-3070 |
| Scheduler 驱动 work loop | ReactFiberWorkLoop.js |
workLoopConcurrentByScheduler |
3073-3079 |
| 完成单元 + 回溯 | ReactFiberWorkLoop.js |
completeUnitOfWork |
3368-3431 |
| 错误回溯 | ReactFiberWorkLoop.js |
unwindUnitOfWork |
3433-3509 |
| 冒泡 flags / lanes | ReactFiberCompleteWork.js |
bubbleProperties |
793-853 |
| 安排根调度(注意:不在 WorkLoop) | ReactFiberRootScheduler.js |
ensureRootIsScheduled |
116-152 |
| 处理调度表 | ReactFiberRootScheduler.js |
processRootScheduleInMicrotask |
259-296+ |
| update queue 类型 | ReactFiberClassUpdateQueue.js |
Update / SharedQueue / UpdateQueue |
131-153 |
| 初始化更新队列 | ReactFiberClassUpdateQueue.js |
initializeUpdateQueue |
176-189 |
| 克隆更新队列 | ReactFiberClassUpdateQueue.js |
cloneUpdateQueue |
191-208 |
| 环形链表入队 | ReactFiberClassUpdateQueue.js |
enqueueUpdate |
223-274 |
| 处理队列(环形链表剪开 / base 重设) | ReactFiberClassUpdateQueue.js |
processUpdateQueue |
504-547 / 660-674 |
| 取"一组"同批车道 | ReactFiberLane.js |
getHighestPriorityLanes |
180-247 |
| 预渲染判定 | ReactFiberLane.js |
checkIfRootIsPrerendering |
412-425 |
| 纠缠车道合并 | ReactFiberLane.js |
getEntangledLanes |
427-475 |
| 创建按位索引数组 | ReactFiberLane.js |
createLaneMap |
815-823 |
| 写入待处理车道 | ReactFiberLane.js |
markRootUpdated |
825-849 |
| 标记挂起车道 | ReactFiberLane.js |
markRootSuspended |
851-885 |
| 标记 ping 车道 | ReactFiberLane.js |
markRootPinged |
887-892 |
| 渲染结束清账 | ReactFiberLane.js |
markRootFinished |
894-995 |
| 取车道位下标 | ReactFiberLane.js |
pickArbitraryLaneIndex / laneToIndex |
776-782 |
| 准备新栈(核心赋值段) | ReactFiberWorkLoop.js |
prepareFreshStack |
2023(核心 2254-2292) |
| 重置 WIP 栈(中断重开) | ReactFiberWorkLoop.js |
resetWorkInProgressStack |
1979-2002 |
| 冒泡(bailout 只冒泡静态 flag) | ReactFiberCompleteWork.js |
bubbleProperties |
793-880 |
| 宿主效果掩码 | ReactFiberFlags.js |
HostEffectMask |
57 |
| Scheduler 让出判据(5ms 帧预算) | packages/scheduler/src/forks/Scheduler.js |
shouldYieldToHost |
459-472 |
| 帧间隔常量 | packages/scheduler/src/SchedulerFeatureFlags.js |
frameYieldMs |
11 |
| Scheduler 主循环(到期才让出) | packages/scheduler/src/forks/Scheduler.js |
workLoop |
193-245 |
十四、自测 FAQ
Q1:alternate 到底是什么?为什么需要它? A:alternate 是"另一个版本的 Fiber"。current 树的每个节点通过它指向 WIP 树对应节点,反之亦然。它是双缓冲成立的必要条件------渲染时 WIP 以 current 为蓝本增量构建(共享 stateNode/memoizedState 等),提交后两棵树角色互换。没有它就无法"后台构建、前台展示"。
Q2:双缓冲会不会让内存翻倍? A:不会全局翻倍。createWorkInProgress 是懒创建 替身(ReactFiber.js:326),只有被更新的子树才有 alternate;静止子树零额外开销。且 WIP 只复制 Fiber 壳、共享大块状态引用,实际增量远小于 2 倍。
Q3:为什么 createWorkInProgress 要写 flags = current.flags & StaticMask? A:动态 effect(Placement/Update 等)每次渲染都要重算,所以先清零;静态 flag(PassiveStatic/LayoutStatic 等,描述"子树是否含某类 effect")跨渲染有效,用 & StaticMask 保留,避免 commit 阶段重算"子树是否含 passive effect"这类信息。
Q4:workInProgress.memoizedState 为什么能直接共享 current 的引用? A:因为 WIP 要基于"上一棵树的结果"推导新结果(例如函数组件的上一次 Hook 状态)。共享引用避免深拷贝。但 dependencies 必须克隆(渲染期会改),这正是 ReactFiber.js:394-407 的做法。
Q5:getHighestPriorityLane 的 lanes & -lanes 是什么魔法? A:-lanes 是补码,它会把 lanes 最低位的 1 保留、其余清零。所以 lanes & -lanes 一行就取出"最紧急(最靠右 bit)的那条车道"。是位运算里经典的"取最低有效位"技巧。
Q6:为什么过期车道要强制同步(renderRootSync)? A:为防止饥饿。markStarvedLanesAsExpired 给每条 pending lane 算过期时间,到点就加入 expiredLanes;performWorkOnRoot 的 shouldTimeSlice 检查 includesExpiredLane,一旦触碰过期车道就走不 yield 的同步渲染,保证它最终能完成(6.5、7.3)。
Q7:getNextLanes 里"DefaultLane 不抢占 TransitionLane"是怎么实现的? A:在 getNextLanes 的中断保护段(ReactFiberLane.js:346-357),当 nextLane === DefaultLane && wipLane 属于 TransitionLanes 时,返回 wipLanes(保持当前 WIP),不打断。源码注释说 transition 和 default 语义接近,但 transition 不支持刷新式 transition,所以允许同批渲染。
Q8:三个 work loop 变体该怎么选? A:workLoopSync 用于过期/强制同步(含 act);workLoopConcurrent 按 25ms/5ms 绝对时间片 yield(故意给动画让路,压到约 30fps);workLoopConcurrentByScheduler 由 Scheduler 的 shouldYield() 决定让出。选择逻辑在 renderRootConcurrent(ReactFiberWorkLoop.js:3014-3018)。
Q9:Fiber 为什么用链表而不是嵌套对象树? A:为了把"深度优先递归"变成"可中断的循环"。递归的调用栈无法被暂停/恢复;链表用 return/child/sibling 把控制流状态显式数据化,while (workInProgress) performUnitOfWork() 任意时刻都能让出主线程,凭 workInProgress 指针无损恢复(第八节)。
Q10:subtreeFlags 有什么用? A:渲染完成后每个节点的 subtreeFlags 精确包含"它整棵子树需要哪些 effect"(bubbleProperties 冒泡得到)。Commit 阶段用 subtreeFlags & MutationMask 之类快速跳过"整棵不需要 DOM 变更/布局/副作用的树",是双缓冲 + 冒泡带来的性能红利。
Q11:DEV 下 Object.preventExtensions(fiber) 是干嘛的?会不会影响性能? A:它冻结 Fiber,防止运行时代码意外加字段破坏"固定 shape"优化,并保证 DevTools 假设成立。注释(ReactFiber.js:175-187)说明:故意把 Profiler 计时字段初始化成 double 而非 SMI,正是为了避免 v8 因"先 SMI 后 double"改变对象 shape 而导致 React DEV 下"unusably slow"。少数 polyfill 不支持时(hasBadMapPolyfill)则不冻结。
Q12:FiberRoot 和 HostRoot 有什么区别? A:FiberRoot 是整个 React 应用的根容器(只有一个),持有全局调度状态(pendingLanes、callbackNode 等);HostRoot 是 Fiber 树的根节点(tag=3),FiberRoot.current 指向它,它 .stateNode 反向指向 FiberRoot,形成循环引用(4.1 节)。
Q13:createWorkInProgress 里分支 A/B 之后为什么还有一段"统一段"? A:因为不论"新建替身"还是"复用替身",都要做同一批字段搬运(flags & StaticMask、共享 child/memoizedProps/memoizedState/updateQueue、克隆 dependencies、拷贝 sibling/index/ref)。ReactFiber.js:381-418 那段统一段就是这些公共动作;把它抽出两个分支能减少重复。副作用是:分支 B 里 359 行的 workInProgress.flags = NoFlags 会在统一段 383 行被再赋一次 为 current.flags & StaticMask,理解时不能孤立看 359 行。
Q14:createWorkInProgress 里 flags 为什么先清零又 & StaticMask? A:两步语义是"先清掉本次渲染的动态 effect(Placement/Update/Ref 等),再保留跨渲染有效的静态 flag(PassiveStatic/LayoutStatic 等)"。直接写 flags = current.flags & StaticMask 其实一步到位,但分支 B 里显式 = NoFlags 是为了"分支内字段一致性"(注释 "Reset the effect tag")。两者叠加效果就是"只在替身上保留静态 flag"。
Q15:未被更新的子树到底会不会分配 WIP Fiber? A:不会 。createWorkInProgress 只在 beginWork 真正走到某节点时才被调用;命中 bailout 的子树,beginWork 不再向子节点递归,那些节点的 alternate 始终为 null。这正是 ReactFiber.js:326-330 注释所说的"lazily created to avoid allocating extra objects for things that are never updated"------静止子树零分配,WIP 树本质是一张"部分新建、部分引用 current 的稀疏补丁"。
Q16:resetWorkInProgress 和 createWorkInProgress 有什么区别?为什么它多保留一个 Placement? A:createWorkInProgress 用于"常规增量构建替身";resetWorkInProgress(ReactFiber.js:440-527)用于"复用同一个 Fiber 做第二遍渲染"(如 Suspense replay),把已被写过的 Fiber 复位成"仿佛刚被创建/克隆"的样子。它保留 Placement(flags &= StaticMask | Placement)是因为 Placement 是子 Fiber 设置给父 Fiber 的协调结论 ,第二遍渲染不该抹掉它,否则父节点会漏掉"要插入子节点"。同时它故意不复位 actualDuration/actualStartTime,以便跨多个 pass 累计实际耗时。
Q17:任务清单里提到的 assignFiberPropertiesInDEV 在哪? A:在本版本(React 19.3.1)的 ReactFiber.js 里不存在 该函数。DEV 下批量拷贝 _debug* 字段的逻辑已内联进 createWorkInProgress(分支 A 的 341-348 行、函数尾部 420-434 行)。本系列以源码为准,特此更正:没有这个独立函数。
Q18:FiberRoot 上 pendingLanes/suspendedLanes/pingedLanes/warmLanes/expiredLanes 分别是什么? A:pendingLanes 是所有待处理车道;suspendedLanes 是因 Suspense 挂起的;pingedLanes 是数据就绪被唤醒的挂起车道;warmLanes 是"已尝试整棵树、确实没得做"的预热车道;expiredLanes 是预算耗尽、必须强制同步的车道。写入者分别是 markRootUpdated/markRootSuspended/markRootPinged/markRootSuspended(didAttemptEntireTree)/markStarvedLanesAsExpired(ReactFiberLane.js:825/851/887/864/583)。
Q19:update queue 的 shared.pending 为什么是环形链表? A:为了在无锁的并发入队下"只写一个 pending 指针"就能完成挂载(u.next = pending.next; pending.next = u; pending = u),避免 head/tail 双指针短暂不一致导致丢更新。处理队列时(processUpdateQueue)会先把环"剪开"再接到 firstBaseUpdate 上(ReactFiberClassUpdateQueue.js:504-547)。
Q20:为什么 updateQueue 要克隆,而 shared 不克隆? A:initializeUpdateQueue(ReactFiberClassUpdateQueue.js:176-189)把队列分成 baseState/firstBaseUpdate/lastBaseUpdate(会被"处理队列"改写)与 shared(装环形 pending)。cloneUpdateQueue(191-215)只克隆前者,让 WIP 与 current 各自持有自己的 base,但共享同一个 shared------因为环形 pending 是"待处理的新更新",两棵树必须看到同一份,否则 WIP 读不到刚入队的更新。
Q21:markRootUpdated 为什么"非 Idle 更新"要清空 suspendedLanes/pingedLanes/warmLanes? A:源码注释(ReactFiberLane.js:832-843)说明:新到的更新可能改变挂起组件的输入从而解开挂起,所以清空挂起/预热标记,给它们重试机会;而 Idle 更新永远在所有常规工作之后才处理,不可能解开 transition,故不清。
Q22:warmLanes / "预热(prewarm)"是什么? A:当一次渲染挂起且 didAttemptEntireTree 为真(整棵树的兄弟都试过了),markRootSuspended 会把该车道记进 warmLanes,表示"这棵树没有可推进的工作了"。getNextLanes 在"既无新鲜更新、也无 ping"时会退回看 pending & ~warmLanes(ReactFiberLane.js:295)------找出"还没预热过"的车道做投机性预渲染;但如果 rootHasPendingCommit(已有成品待提交),则不预热(266-277 行注释:丢掉成品去猜不划算)。
Q23:shouldYield() 的 5ms 从哪来?能改吗? A:来自 Scheduler fork 的 frameInterval,初值 frameYieldMs = 5(packages/scheduler/src/SchedulerFeatureFlags.js:11);shouldYieldToHost(packages/scheduler/src/forks/Scheduler.js:459-472)在"距本帧开始超过 5ms"时返回 true。它能改:forceFrameRate(fps) 把 frameInterval 设为 Math.floor(1000/fps)(fps 0..125,0 则复位)。另有旁路------requestPaint() 置 needsPaint 后 shouldYieldToHost 无条件返回 true,立即让出以绘制。
Q24:workLoopConcurrent 的 25ms/5ms 和 Scheduler 的 5ms 是一回事吗? A:不是两套独立机制。workLoopConcurrentByScheduler 用 Scheduler 的 shouldYield()(5ms 帧预算);而 workLoopConcurrent 自己用 now() + (nonIdle?25:5) 算绝对截止时间,不走 shouldYield 。源码注释(ReactFiberWorkLoop.js:3057-3062)强调其目的不是降低 yield 开销,而是故意把主线程让给动画,把动画帧率压到约 30fps:非 idle 用 25ms、idle 用 5ms。
Q25:act 环境为什么不 yield? A:因为测试环境里 shouldYield 依赖的 host I/O 多半是 mock 的,返回值不可信。renderRootConcurrent 在 __DEV__ && ReactSharedInternals.actQueue !== null 时直接走 workLoopSync(ReactFiberWorkLoop.js:3007-3013),保证"一次 act 就把整棵树渲染完",断言才有确定性。这也解释了"同一组件在测试与浏览器行为可能不同"。
Q26:bubbleProperties 的 didBailout 怎么判断?bailout 时冒泡什么? A:判据是 completedWork.alternate !== null && completedWork.alternate.child === completedWork.child(ReactFiberCompleteWork.js:794-796)------即 WIP 的 child 指针与 current 替身的 child 完全相同,说明没做新协调。bailout 分支只冒泡 StaticMask 内的静态 flag (subtreeFlags |= child.subtreeFlags & StaticMask,875-876 行),因为动态 flag 只活单次 render+commit,静态 flag 才与 Fiber 同生命周期。
Q27:unwindUnitOfWork 里 next.flags &= HostEffectMask 是清什么? A:HostEffectMask(ReactFiberFlags.js:57)的低 15 位全 1,覆盖"单次 commit 生命周期"的宿主效果(PerformedWork 到 StoreConsistency)。这行(ReactFiberWorkLoop.js:3454)在"找到错误边界、准备重入 begin 阶段"时,剔除位号 ≥15 的非宿主标记 (如 Incomplete/ShouldCapture/NeedsPropagation),只保留宿主 commit 相关 flag,避免脏标记进入后续阶段。
Q28:ReactFiberStack.js 的 valueStack 和渲染进度是什么关系? A:valueStack 是 push/pop 共用的模块级数组,用显式栈替代"依赖调用栈的隐式上下文"。它要求 push/pop 严格 LIFO 配对 ,并且必须与渲染进度同步。当一轮渲染被放弃、要重开时,resetWorkInProgressStack(ReactFiberWorkLoop.js:1979-2002)会沿 return 回溯、对每个已进入的 Fiber 调 unwindInterruptedWork 弹出上下文游标,防止 valueStack 残留上一轮的压栈记录。双缓冲负责"Fiber 树可丢弃重来",它负责"游标栈也能跟着丢弃重来"。
十五、本章要点小结
- Fiber 是"实例 + 工作单元"的合体:前半字段(tag/type/stateNode)描述"组件是什么",后半字段(child/sibling/return/flags/lanes/alternate)描述"渲染进行到哪"。
- 链表而非递归 :
child/sibling/return把控制流数据化,使 work loop 可中断可恢复------这是 Fiber 取代 Stack Reconciler 的根本。 - 双缓冲 = current + workInProgress + alternate 互指 :后台增量构建、前台永远可用,靠
root.current指针翻转切换;替身懒创建,静止子树零额外内存。 createWorkInProgress是双缓冲引擎:无替身则新建(共享实例字段),有替身则复用(重置动态 flag、保留静态 flag、共享状态引用、克隆 dependencies)。- Flags 是"先渲染后提交"的枢纽 :单个 flag 标记节点副作用,分组 mask(Mutation/Layout/Passive/Static)是 Commit 阶段的路由表,靠
subtreeFlags冒泡实现快速跳过。 - Lane 是 31 位优先级位掩码 :
getHighestPriorityLane = lanes & -lanes取最高优先级;getNextLanes实现中断保护与"DefaultLane 不抢占 TransitionLane";computeExpirationTime+markStarvedLanesAsExpired用时间兜底防止饥饿,过期车道强制同步。 - 三个 work loop 变体对应三种让出策略:同步(不 yield)、时间片(25/5ms)、Scheduler 驱动(shouldYield)。
- 位掩码优于 expirationTime:天然支持多优先级并存、集合运算、整组车道共享调度策略;时间戳只作防饥饿兜底。
- 衔接点 :Scheduler(第 5 章)提供执行时机与 shouldYield;DOM/Commit(第 6 章)消费 flags;Hooks(第 7 章)状态藏在
memoizedState链表上。
下一章(建议第 4 章:beginWork 与协调算法 / 或第 5 章:Scheduler 与优先级映射)会站在本章的地基之上,进一步展开"beginWork 如何协调出子节点"以及"Lane 如何映射到宿主调度优先级"。建议先吃透本章的
createWorkInProgress与performUnitOfWork,再去读上层才不会迷失在调用链里。