Skip to main content
Fiber 我是顺着”渲染为什么能中断”这个问题看进去的:先记住它是链表,再弄懂 beginWork/completeWork 的分工,最后才理解 Lanes 怎么排优先级。

Q1: 为什么需要 Fiber?Stack Reconciler 的局限是什么?

Andrew Clark 在其 React Fiber Architecture 文档中开宗明义:“A fiber represents a unit of work”[1]。Fiber 的诞生源于一个根本性的架构矛盾——React 15 的 Stack Reconciler 用原生递归调用栈做 diff,而 JavaScript 的调用栈是后进先出结构,无法在中间状态暂停并事后恢复。
1

16.6ms 的渲染帧预算

现代浏览器为了保持 60fps 的流畅度,要求每一帧的执行时间必须在 16.6ms 以内。在这 16.6ms 中,浏览器需要执行 JavaScript 脚本、计算样式(Style)、执行布局(Layout)和重绘(Paint)。JS 执行的时间预算通常只有 5-8ms。
2

同步递归霸占主线程

React 15 的 Stack Reconciler 用原生 JavaScript 递归调用对比虚拟 DOM。Lin Clark 在其经典漫画演讲中形象地解释了这个问题:“如果你只依赖调用栈,它会一直工作到栈为空。在 UI 渲染场景下,这意味着浏览器必须等到整棵组件树 diff 完成才能响应用户输入”[2]。组件树嵌套越深,主线程被霸占的时间越长。
3

掉帧与卡顿的视觉灾难

一次深度的 setState 可能占用主线程 50ms 甚至 100ms 以上。由于主线程被完全霸占,浏览器无法按时执行绘制,用户看到的就是页面无响应、输入框阻塞、动画掉帧。
4

时间切片的需求

为了解决这个问题,必须将巨大的更新任务拆分成若干个微小的执行单元(Fiber 节点),每执行完一个单元就检查是否还有剩余时间。如果没有剩余时间,就将控制权交还给浏览器。Andrew Clark 将这种能力描述为”The ability to split rendering work into chunks and spread it out over multiple frames”[1]。
5

重构底层架构的决心

由于 JavaScript 原生调用栈无法在中间状态暂停并事后恢复,React 团队彻底放弃了递归算法,转而在用户态实现了一个带有状态保存功能的虚拟调用栈——Fiber 架构。Andrew Clark 解释道:“Fibers serve as a reimplementation of the call stack, specialized for React components… these virtual stack frames can be kept in memory and executed at will”[1]。
Fiber 的诞生就像工厂从”强制加班制”改成了”弹性工时制”。React 15 的 Stack Reconciler 就像一个不允许中途下班的工人——接到任务(setState)后必须一口气干完(同步递归),干不完不能走(霸占主线程),结果整个工厂的其他部门(浏览器渲染)都停工了。Fiber 就像引入了弹性工时——每干完一个小任务(一个 Fiber 节点),就看看表(shouldYield),到点了就打卡下班(让出主线程),明天接着干(从 nextUnitOfWork 恢复)。从此工厂的生产(UI 渲染)和物流(浏览器绘制)可以并行运转了。
Fiber 解决的核心问题:
Fiber 让 React 从”一口气跑完”变成了”跑一段歇一下”,从根本上解决了同步渲染的阻塞问题。

原生调用栈的限制

原生的 JS 函数调用栈(Call Stack)缺乏主动让权(Yield)机制,必须按严格的后进先出顺序执行至栈空。就像一口气跑到山顶,中途不能停下休息,跑不动了只能硬撑到终点。

Fiber 虚拟调用栈

将协调过程从依赖原生调用栈,转变为由自己控制执行周期的链表数据结构。就像爬山时修了台阶,每走一段就有休息平台,随时可以停下来欣赏风景。
回答时先说”为什么”(16.6ms 帧预算 + 同步递归阻塞),再说”怎么解决”(Fiber 时间切片)。想听的是你对架构决策背后性能约束的理解,而不是背诵 API。

Q2: Fiber 节点的链表结构是如何替代递归的?

Fiber 节点的链表树设计

Fiber 的核心目标是实现一种针对 UI 渲染优化的虚拟调用栈。每一个 Fiber 节点本质上就是一个普通的 JavaScript 对象,但它通过三个指针(return、child、sibling)构建出一棵单链表树结构,使得 React 可以用深度优先的循环迭代(while 循环)替代传统的递归调用。Andrew Clark 将这种结构描述为”child-sibling representation”,并类比尾调用:“a child fiber as a tail-called function”[1]。这种设计使得遍历不需要递归——调度器可以迭代式地走遍整棵树,随时暂停和恢复。

Fiber 节点的核心属性

每个 Fiber 节点包含三类信息:身份标识(tag、key、type)、链表指针(return、child、sibling)、状态与副作用(memoizedState、updateQueue、flags)。其中 tag 标识节点类型(0=FunctionComponent、1=ClassComponent、5=HostComponent),flags 标识需要执行的副作用(Placement=2、Update=4、ChildDeletion=16,其中 ChildDeletion 标记在父节点上;React 18 没有单独的 Deletion 标记)。

Reconciler 原理:由简到难的讲解

Reconciler(协调器)的核心工作是对比新旧虚拟 DOM 树,找出需要更新的节点。React 15 的 Stack Reconciler 用递归实现,React 16 的 Fiber Reconciler 用迭代实现。协调过程分两步:第一步是 beginWork(从父到子,创建/更新 Fiber 节点,打上 flags 标记),第二步是 completeWork(从子到父,创建真实 DOM 节点,收集副作用列表)。
虽然结构变成了链表,但 React 的遍历顺序依然是”深度优先”。先找到最深处的子节点,然后再找兄弟节点,最后回到父节点。如果在中间因为时间切片被中断,React 只需要保存当前正在处理的那个 Fiber 节点指针(nextUnitOfWork),下一帧就可以从该指针精准恢复——这是递归做不到的。
原生递归就像玩一命通关的《超级马里奥》,一旦开始就不能停,中途死了或有事(时间用完)就必须重头再来。Fiber 就像在每个关卡增加了存档点(保存 nextUnitOfWork 指针)。时间不够了?没关系,在当前的 Fiber 节点存个档,去干别的事(让浏览器绘制),回来后直接读取存档继续打,不用从 1-1 重新开始。

Fiber 节点的完整数据结构详解

Fiber 节点在源码中由 FiberNode 构造函数生成,除了核心的链表指针外,还包含大量辅助属性。理解这些属性对阅读 React 源码至关重要。

tag 常见值与对应节点类型

flags 副作用标记系统

React 用位运算标记 Fiber 节点的副作用类型,多个 flags 可以通过按位或合并:

Q3: Reconciler 工作流程详解——由简到难

协调器的本质:Diff 算法与渲染分离

Andrew Clark 在 Fiber Architecture 文档中定义了 Reconciler 的核心职责:“The algorithm React uses to diff one tree with another to determine which parts need to be changed”[1]。协调和渲染是两个独立的阶段——协调器计算哪些部分变了,渲染器负责实际更新环境(DOM、React Native 等)。这种分离让不同的渲染器可以共享同一套协调逻辑。

第一层:整体流程概览

Reconciler(协调器)的工作分为两大阶段:Render 阶段(可中断,在内存中计算差异)和 Commit 阶段(不可中断,应用到真实 DOM)。

第二层:beginWork 详解

beginWork 是 Render 阶段的”向下遍历”过程。它从根节点开始,对每个 Fiber 节点执行以下操作:
beginWork 的返回值决定了下一个要处理的节点:
  • 返回 child:继续向下遍历子节点
  • 返回 null:没有子节点,开始 completeWork

第三层:completeWork 详解

completeWork 是 Render 阶段的”向上回溯”过程。当遍历到叶子节点(没有 child)时,开始 completeWork。
completeWork 的关键作用:
  1. 创建或更新真实 DOM:挂载时对 HostComponent 调用 createInstance,更新时走 updateHostComponent
  2. 处理属性:挂载时调用 finalizeInitialChildren 设置初始 DOM 属性
  3. 冒泡副作用:通过 bubbleProperties 把自身和子树的 flags 汇总到 subtreeFlags

第四层:commitWork 详解

Commit 阶段是不可中断的,分为三个子阶段:
Render 阶段的 beginWork 就像建筑师画蓝图——从大楼外观到每个房间的布局,逐层设计并标记”这里要建墙、那里要开窗”。completeWork 就像施工队从最底层开始建造——先建好每一层的房间,再向上盖楼。Commit 阶段的 beforeMutation 就像拍一张”施工前”的照片(快照)。mutation 就像正式施工——拆墙、砌墙、装窗户(DOM 操作)。layout 就像装修完成后的验收——安装家具、挂窗帘、接通水电(副作用执行)。

Q4: 双缓冲机制详解

双缓冲就像画家在画廊现场作画——他不会直接在展厅墙上画(单缓冲,观众会看到画到一半的半成品),而是准备两块画布:一块挂在墙上给观众看(current 树),另一块在后台悄悄画新作品(workInProgress 树)。等新作品画完了,一秒钟换上去(指针切换),观众看到的永远是完整的作品。两块画布通过 alternate 标签互相关联,下次画画时可以直接在旧画布上修改(节点复用),不用每次都买新画布。

双缓冲的图形学起源

双缓冲(Double Buffering)的概念来自图形学——游戏引擎在后台渲染下一帧,渲染完成后一次性替换到屏幕上,避免用户看到”绘制一半”的画面。React 的 current 和 workInProgress 两棵树正是这一思想在 UI 协调中的应用。

为什么需要两棵树?

如果只维护一棵 Fiber 树,每次更新都需要直接修改当前树的节点。如果更新被中断,用户看到的将是一个”半更新”的破损 UI。

alternate 属性的复用机制

两棵 Fiber 树通过 alternate 属性互相指代。更新时,React 不会销毁旧节点重建新节点,而是复用旧的 Fiber 节点,只更新变化的属性。

双缓冲的核心优势


Q5: 时间切片原理详解

时间切片就像程序员用”番茄工作法”——每工作 5 分钟(5ms 切片)就休息一下(让出主线程给浏览器渲染),而不是连续加班 3 小时(同步递归 100ms)。shouldYield() 就是你的闹钟——“叮!5 分钟到了,该休息了!“。休息完后,你看看桌上的便利贴(nextUnitOfWork 指针),就知道从哪里接着干。MessageChannel 就是你设定的”自动续时”机制——一个番茄钟结束后,自动开始下一个番茄钟,直到所有任务完成。

调度的核心思想:Pull 模型

Andrew Clark 在 Fiber Architecture 文档中引用了 React 的设计原则:“React is not a generic data processing library. It is a library for building user interfaces”[1]。因此调度采用 Pull 模型——“computations can be delayed until necessary”,而不是 Push 模型(新数据一来就立即执行所有计算)。React 根据任务优先级决定何时执行、何时让出。

时间切片的执行流程

shouldYield 的实现

为什么是 5ms 而不是 16.6ms?

5ms 是 React 内部的切片单位,而不是帧预算。一帧 16.6ms 中,浏览器要在 JS 执行、Layout、Paint、Composite 等步骤间分配时间;下面的 ~5ms、~4ms、~3ms、~1ms 只是示意性的示例数值,并非固定预算。React 选择 5ms 作为切片单位,是为了给浏览器渲染和其他任务留出足够的时间。
时间切片只能拆分多个小任务,无法拆分单个大任务。如果一个 Fiber 节点的渲染函数本身就需要 50ms(比如一个超复杂的计算),时间切片无法帮助你——因为单个 Fiber 节点的 beginWork 是原子执行的。解决方案是将大组件拆分成小组件,让每个组件的渲染时间都在 5ms 以内。

Render 阶段与 Commit 阶段的本质区别

Andrew Clark 在 Fiber Architecture 文档中解释了两阶段分离的设计:“Reconciliation does the work of computing which parts of a tree have changed; the renderer then uses that information to actually update the rendered app”[1]。

基础梳理(Render vs Commit 对比)

专家视角深度剖析

很多人只能说出”Render 阶段可中断、Commit 阶段不可中断”,但实际更在意的是:为什么 Commit 阶段绝对不能中断? 答案在于用户看到的 UI 必须是原子化的一致状态。如果 Commit 阶段执行到一半被中断,用户会看到一个”半新半旧”的破损 UI——比如列表只更新了一半,按钮样式变了但文字没变。这比卡顿更糟糕,因为它会破坏用户对界面的信任。 Render 阶段之所以可以中断,是因为它只在内存中操作 Fiber 节点的 flags(副作用标记),不涉及任何真实 DOM 操作。中断后重新开始,只需要从上一次中断的 Fiber 节点继续遍历即可,之前计算的中间结果通过 workInProgress 树保留。 这也解释了为什么 React 废弃了 componentWillMount 等生命周期——它们在 Render 阶段执行,但开发者经常在里面写副作用代码(如发请求、操作 DOM)。一旦 Render 阶段被中断和重试,这些副作用会被多次执行,导致不可预测的 bug。

Q6: Fiber 节点在源码里长什么样?

FiberNode 构造函数

在 React 源码中,Fiber 节点由 FiberNode 构造函数生成,包含了节点信息、链表指针以及副作用标记。详见 React 源码 packages/react-reconciler/src/ReactFiber.js。
这段代码做了 3 件事:第一,tag、key、type 定义了节点的基本身份;第二,return、child、sibling 三个指针构成了链表树的骨架,替代了原生递归调用栈;第三,flags 和 alternate 分别支撑了副作用标记系统和双缓冲机制。

时间切片的调度实现

详见 React 源码 packages/react-reconciler/src/ReactFiberWorkLoop.js:
React 选择 MessageChannel 而不是 requestAnimationFrame 来调度时间切片,因为 MessageChannel 的回调是纯粹的宏任务,不受屏幕刷新率限制,可以更高效地压榨 CPU 空闲时间。

Q7: 及格回答和满分回答差在哪?

问题:“说说 React Fiber 架构”

及格回答(60-70分):
“Fiber 是 React 16 引入的新架构,用链表替代递归,实现了时间切片,解决了同步渲染卡顿的问题。”
问题出在:知道结论但不理解机制。 满分回答(90-100分):
Fiber 解决的核心问题是 16.6ms 帧预算下的同步阻塞。React 15 的 Stack Reconciler 用原生递归对比虚拟 DOM,一旦开始就无法中断,大型应用一次 setState 可能霸占主线程 100ms 导致掉帧。React 16 彻底放弃递归,用 Fiber 节点的 child、sibling、return 三个指针构建虚拟调用栈,通过 while 循环实现深度优先遍历。
时间切片的核心在于 shouldYield() 检查。每执行完一个 Fiber 节点,React 会检查距离当前帧截止时间是否不足 5ms,如果是就通过 MessageChannel 让出主线程,下一帧再从 nextUnitOfWork 指针恢复。这种”存档-恢复”机制是原生递归做不到的。
双缓冲机制让 React 同时维护 current 和 workInProgress 两棵 Fiber 树。Render 阶段在 workInProgress 树上计算差异并打上 flags 标记,Commit 阶段一次性同步应用到真实 DOM,然后通过 alternate 属性切换指针。节点通过复用而非销毁重建,极大降低了 V8 的 GC 压力。

常见追问

答题要点: 从调度频率和帧同步两个维度对比。高分回答: “requestAnimationFrame 的执行频率受限于浏览器刷新率(60Hz,16.6ms),后台标签页甚至会降到极低。React 需要的是一种尽可能高频、不受屏幕刷新率限制的宏任务调度器来压榨 CPU 空闲时间。MessageChannel 是纯粹的宏任务,在事件循环中优先级高,且不受显示器刷新率的物理限制。如果只需要在每帧绘制前做一次视觉更新,用 rAF;如果是为了实现高频的微小任务切片调度,MessageChannel 是最佳选择。未来浏览器原生支持 scheduler.postTask 后,React 可能会切换到原生 API。”
答题要点: 结合中断恢复和生命周期废弃来解释。高分回答: “Render 阶段是纯内存计算,不涉及真实 DOM 操作,所以可以被中断和恢复。当高优先级更新(如用户输入)到来时,React 会中断当前低优先级的 Render,转而处理高优先级任务。中断后,之前低优先级的 Render 阶段需要从头重新开始(因为 workInProgress 树可能已经过时)。这意味着 Render 阶段可能被执行多次。这也是 React 废弃 componentWillMount 等生命周期的根本原因——它们在 Render 阶段执行,但开发者经常在里面写副作用,被多次执行会导致不可预测的 bug。”
答题要点: 从 GC 压力和 UI 一致性两个角度解释。高分回答: “React 同时维护 current(屏幕显示)和 workInProgress(后台构建)两棵 Fiber 树。Render 阶段复用 current 树的节点来构建 workInProgress 树,仅对变化的属性更新。Commit 阶段完成后,通过 alternate 属性切换指针。这种设计有两个核心好处:第一,避免了每次更新都销毁和重建上万个 Fiber 对象,极大降低了 V8 的 GC 压力;第二,确保用户看到的 UI 永远是完整的一致状态——要么是旧树,要么是新树,不会出现半更新的中间态。灵感来自图形学的双缓冲技术。”
答题要点: 从死循环防护和实际踩坑场景两个角度回答。高分回答: “Render 阶段本身就是在处理状态更新,如果在处理更新的过程中又触发了新的状态更新,系统会陷入无限循环。React 检测到正在处于 render 阶段时,会立即抛出 ‘Too many re-renders’ 错误来阻止死循环。实际踩坑场景中,这常常发生在新接手项目的同学在组件主体中直接写了 setCount(count + 1) 而忘记放入 useEffect 或事件处理函数中。解决方案:所有的 setState 操作都必须被约束在副作用(useEffect)或事件处理函数(如 onClick)中。”
答题要点: 从遍历方向和核心职责两个维度对比。高分回答: “beginWork 是从父到子的深度优先遍历,核心职责是根据新的 JSX 创建或复用 Fiber 节点,调用 reconcileChildren 对比新旧子节点,打上 flags 标记(Placement/Update/Deletion)。completeWork 是从子到父的回溯过程,核心职责是为 HostComponent 创建真实 DOM 节点(createElement),处理属性(finalizeInitialChildren),并将子树的副作用冒泡到父节点(subtreeFlags)。两者配合完成了 Render 阶段的全部工作:beginWork 标记’要做什么’,completeWork 准备’怎么做’。”
答题要点: 从 UI 渲染的一致性需求解释。高分回答: “深度优先遍历保证了子节点一定在父节点之前完成 completeWork。这意味着当父节点执行 completeWork 时,所有子节点的 DOM 已经创建完毕,可以直接 appendChild 到父节点的 DOM 上。如果是广度优先,父节点可能在子节点之前完成,此时无法将子节点的 DOM 挂载到父节点上。从 UI 角度看,深度优先也更符合’先处理最内层组件,再向外扩展’的渲染逻辑——内层组件的 DOM 必须先就绪,外层容器才能正确布局。”
答题要点: 从位标记和批量处理两个角度解释。高分回答: “React 用位运算来标记 Fiber 节点的副作用类型。稳定的两个标记是 Placement(值为 2,需要插入 DOM)和 Update(值为 4,需要更新属性);删除在历史版本里经历过 Deletion 单独标记到 ChildDeletion 标记在父节点上的演进,具体位次不要硬编码。多个 flags 可以通过按位或合并到一个节点上。Commit 阶段遍历 Fiber 树时,通过按位与判断节点是否有某个标记。子树的 flags 通过 subtreeFlags 冒泡到父节点——如果 subtreeFlags 为 0,说明整棵子树没有任何副作用,Commit 阶段可以直接跳过,这是重要的性能优化。”
答题要点: 从指针切换和节点复用两个角度解释。高分回答: “每次更新开始时,React 根据 current 树的根节点创建 workInProgress 树的根节点(通过 createWorkInProgress 复用或创建)。Render 阶段在 workInProgress 树上计算差异,Commit 阶段将差异应用到真实 DOM 后,执行 root.current = finishedWork,将根节点的 current 指针从旧树切换到新树。旧树的节点不会立即被 GC,而是保留为下一次更新的 alternate。下次更新时,React 通过 createWorkInProgress/useFiber 复用或克隆一个 alternate Fiber,直接在其上更新属性,不需要从头创建新对象。这种’复用+切换’的模式就是双缓冲的核心。”
答题要点: 不要只说”时间切片”,要从五个维度系统阐述 Fiber 架构带来的根本性改变。高分回答: “Fiber 的价值不是某一个单点优化,而是一整套针对 UI 渲染场景的系统性解决方案。可以从五个维度来看:1. 可中断渲染(时间切片)Stack Reconciler 用原生递归,一旦开始就无法中断。Fiber 用链表替代递归,每执行完一个节点就通过 shouldYield() 检查是否需要让出主线程。中断后只需保存 nextUnitOfWork 指针,下一帧精准恢复:
2. 优先级调度Fiber 架构引入了 Lane 优先级模型,高优先级更新(用户输入)可以打断低优先级更新(数据预取)。这在 Stack Reconciler 中完全不可能——所有更新按 FIFO 顺序同步执行,用户输入被大列表渲染阻塞:
3. 双缓冲(中断安全)Fiber 同时维护 current 和 workInProgress 两棵树。Render 阶段在 workInProgress 树上工作,中断不影响屏幕显示的 current 树。Commit 完成后一次性切换指针,用户永远看到完整的 UI,不会出现半更新的中间态。4. 副作用标记系统(subtreeFlags 冒泡优化)Fiber 用 flags 和 subtreeFlags 标记需要执行的副作用。Commit 阶段如果发现某棵子树的 subtreeFlags === 0,直接跳过整棵子树,避免无意义的遍历。这是 Stack Reconciler 没有的优化。5. 并发模式的基础Fiber 是 React 18 并发特性的底层基础。useTransition、useDeferredValue、Suspense 等特性都依赖 Fiber 的可中断和可恢复能力。没有 Fiber,这些 API 在架构上就不可能实现。总结:Fiber 解决的核心问题是让 React 拥有了对渲染过程的主动控制权——可以中断、可以恢复、可以设置优先级、可以跳过无变化的子树。Stack Reconciler 把控制权交给了 JavaScript 引擎的调用栈(后进先出、无法暂停),Fiber 把控制权拿回了 React 自己手中。“

延伸阅读与引用参考