React Fiber 是 React 16 引入的全新核心算法(协调引擎),它的出现彻底改变了 React 的底层运行机制。要理解 Fiber,我们需要从“它解决了什么问题”开始,然后逐步深入它的核心数据结构和运行原理。
1. 背景:为什么需要 Fiber?(React 15 的痛点)
在 React 15 及之前的版本中,React 使用的是栈协调器(Stack Reconciler)。当组件状态发生变化时,React 会同步地、递归地从根节点向下对比(Diff)整个虚拟 DOM 树,并找出需要更新的节点。
- 痛点(主线程阻塞): 这种递归对比是同步的,一旦开始就无法中断。如果组件树非常庞大,对比过程可能会花费很长时间(超过 16.6ms,即 60Hz 刷新率下的一帧时间)。在这段时间内,浏览器的主线程被 React 完全霸占,无法处理用户的点击输入、动画渲染等任务,导致页面出现严重的卡顿(Jank)和掉帧。
2. 核心概念与原理
React Fiber 的核心目标是解决主线程阻塞问题,实现可中断的异步渲染。为了达到这个目标,React 引入了以下几个核心设计:
A. 核心数据结构:Fiber 节点 (链表树)
为了让渲染过程“可中断”和“可恢复”,React 不能再使用依赖 JavaScript 调用栈的递归。Fiber 引入了一种新的数据结构:Fiber Node。
每一个 React 元素(组件或 DOM 节点)在内存中都有一个对应的 Fiber 节点。这些节点通过三个指针链接成一个单链表树:
child: 指向第一个子节点。sibling: 指向下一个兄弟节点。return(或 parent): 指向父节点。
这种结构允许 React 将渲染工作拆分成一个个小单元(每个 Fiber 节点就是一个工作单元)。工作可以随时暂停,保存当前指针状态,稍后顺着链表继续执行。
1// 简化的 Fiber 节点 JS 结构示例
2const fiberNode = {
3 // 1. 实例相关
4 type: 'div', // 组件类型或 DOM 标签名
5 stateNode: null, // 指向对应的真实 DOM 节点或组件实例
6
7 // 2. 构建链表树(核心指针)
8 return: parentFiber, // 指向父节点
9 child: childFiber, // 指向第一个子节点
10 sibling: nextSiblingFiber, // 指向右侧的兄弟节点
11
12 // 3. 状态与工作相关
13 pendingProps: {}, // 即将应用的新 props
14 memoizedProps: {}, // 上次渲染使用的 props
15 memoizedState: null, // 上次渲染的状态(Hooks 数据就存在这里)
16
17 // 4. 副作用标记 (Effect Tag)
18 flags: 0, // 标记该节点需要进行什么操作(如插入、更新、删除 DOM)
19
20 // 5. 双缓存机制
21 alternate: null, // 指向上一棵树的对应节点
22};B. 时间切片 (Time Slicing)
这是 Fiber 实现流畅体验的法宝。React 将原本一长串的同步渲染任务,切分成了许多微小的任务片段(Slices)。
在浏览器的每一帧中,React 会向浏览器“申请”一段时间(例如 5ms)。React 在这几毫秒内拼命处理 Fiber 节点:
- 如果时间用完了,但任务还没做完,React 会主动交出主线程控制权(Yield)。
- 浏览器接管主线程,去处理高优先级的任务(如用户输入、CSS 动画渲染)。
- 等浏览器闲下来了,React 再接手,从上次暂停的 Fiber 节点恢复执行。
C. 两大工作阶段 (Render & Commit Phase)
Fiber 的工作流被严格划分为两个阶段:
- Render 阶段(协调阶段,可中断):
- 在这个阶段,React 会遍历 Fiber 树,调用组件的
render方法(或执行 Function Component),比较新老状态(Diffing),并为需要更新的节点打上标签(flags/effects)。 - 重点: 这个阶段仅仅是在内存中计算,不会触碰真实的 DOM。因此,这个阶段是可以被完全中断、废弃和重做的。
- 在这个阶段,React 会遍历 Fiber 树,调用组件的
- Commit 阶段(提交阶段,不可中断):
- 一旦 Render 阶段完成,React 知道到底需要对 DOM 做哪些修改了,就会进入 Commit 阶段。
- 重点: 这个阶段会一次性将所有的 DOM 变更(增删改)应用到真实页面上,并执行生命周期钩子(如
componentDidMount,useEffect)。为了防止用户看到渲染了一半的 UI,这个阶段是同步的、绝对不可中断的。
D. 双缓存机制 (Double Buffering)
类似于图形学中的双缓冲,React 在内存中同时维护着两棵 Fiber 树:
- Current Tree(当前树): 当前屏幕上显示的内容对应的树。
- WorkInProgress Tree(工作树): 正在内存中构建的新树(在 Render 阶段构建)。
当 WorkInProgress 树构建完毕,进入 Commit 阶段并将真实的 DOM 更新后,React 只需要将根节点的指针从 Current 树切换到 WorkInProgress 树,WorkInProgress 就变成了新的 Current 树。这大大提高了内存重用的效率并保证了 UI 的一致性。
这是一份非常高质量的 React 进阶知识清单。作为你的 AI 合作伙伴,我将为你梳理这些核心知识点,并结合 React 18/19 的最新语境,用最硬核且易懂的方式为你拆解。
1. React Fiber 架构:从“一气呵成”到“见缝插针”
Fiber 是什么?
Fiber 既是一种数据结构(保存组件状态、节点关系的链表),也是一个工作单元。
- 为什么需要它? 在 Fiber 之前(Stack Reconciler),React 更新是同步且不可中断的。如果组件树很深,主线程会被 JS 引擎长期占用,导致浏览器无法响应输入或动画,产生卡顿。
- 核心目标: 实现可中断、可恢复的异步渲染。
工作原理:双缓冲与时间切片
- 双缓冲 (Double Buffering): React 同时维护两棵 Fiber 树:
current(当前屏幕显示的)和workInProgress(正在内存中构建的)。更新完成后,只需交换根节点的指针,即可瞬间完成替换。 - 时间切片 (Time Slicing): React 将长任务拆分成多个小的工作单元。利用
requestIdleCallback的思想(实际是自己实现的 Scheduler),在浏览器空闲时执行更新。
两个阶段
- Render 阶段(可中断): 深度优先遍历 Fiber 树,计算差异(Diff),打上 EffectTag。此阶段由于不操作 DOM,可以被高优先级任务中断。
- Commit 阶段(不可中断): 将 Render 阶段生成的 EffectList 应用到真实 DOM 上。
2. Hooks 原理:闭包与链表的艺术
useState 的实现
每个组件对应的 Fiber 节点上有一个 memoizedState 属性,它是一个单向链表。
- 为什么不能在条件语句中使用? 因为 React 依赖 Hook 调用的固定顺序来关联状态。如果顺序乱了,React 就无法通过链表索引找到对应的状态。
1// 极简示意代码
2function mountState(initialState) {
3 const hook = {
4 memoizedState: initialState,
5 next: null,
6 queue: []
7 };
8 // 将 hook 添加到当前 Fiber 的 hooks 链表末尾
9 return [hook.memoizedState, dispatchAction];
10}useEffect 执行时机
- 渲染后执行:
useEffect会在浏览器完成布局与绘制(Paint)之后异步执行,避免阻塞渲染。 - 清理机制: 在下一次 Effect 执行前或组件卸载前,执行上一次返回的 cleanup 函数。
3. 并发特性 (Concurrent Mode)
并发不是“同时执行”,而是**“具备同时处理多个任务的能力”**。
- Transitions (useTransition): 将状态更新标记为“非紧急”。React 会优先保障输入、点击等交互的流畅,稍后再渲染 UI 更新。
- Automatic Batching: React 18 之后,无论是在 Promise、setTimeout 还是原生事件中,多次
setState都会自动合并为一次渲染。
4. 性能优化:按需取用
不要过度优化,React 的 Diff 算法已经足够快。
- React.memo: 针对 Props 的浅比较。
- useMemo / useCallback: *
useMemo缓存计算结果。useCallback缓存函数引用(主要用于防止子组件因 Prop 函数引用变化而误触发重绘)。
虚拟列表原理
只渲染可视区域内的 DOM 元素。通过监听滚动事件,计算偏移量,动态替换渲染的数据项。
5. React 19 新特性预警
React 19 正在让框架变得更“自动”:
- React Compiler: 未来可能不再需要手动写
useMemo和useCallback,编译器会自动优化。 - Server Components (RSC): 组件默认在服务端运行,减少发送到客户端的 JS 体积。
- Actions: 简化表单提交和异步状态管理,自动处理 Pending 状态。
这是一个非常深刻的问题!很多人在初学 Fiber 时,会误以为 React 使用了浏览器原生的 requestIdleCallback API 来实现“申请”空闲时间。
但实际上,React 团队并没有使用 requestIdleCallback,而是自己实现了一个独立的任务调度器:Scheduler。
React 具体是怎么向浏览器“申请”控制权的呢?核心答案是:利用 MessageChannel 触发宏任务(Macro-task)与 performance.now() 计算耗时。
下面我为你拆解它的具体实现方式:
1. 为什么不用 requestIdleCallback 或 setTimeout?
在揭晓答案前,我们先看看 React 为什么淘汰了那些看似“顺理成章”的原生 API:
- 淘汰
requestIdleCallback:- 兼容性差: Safari 至今对它的支持都很差。
- 触发频率太低: 它的设计初衷是在浏览器“绝对空闲”时才执行。有时候浏览器为了省电,会把它的触发频率降到 50ms 甚至 50hz 才会执行一次。这对于要求 60fps(每帧 16.6ms)流畅度的 UI 渲染来说,实在太慢了。
- 淘汰
setTimeout(fn, 0):- 4ms 延迟硬伤: 根据 HTML5 标准,如果
setTimeout嵌套调用超过 5 层,浏览器会自动强制加上至少 4毫秒 的最小延迟。React 的时间切片本身就只有 5ms,如果每次让出控制权都要被硬塞 4ms 的延迟,性能损耗是完全无法接受的。
- 4ms 延迟硬伤: 根据 HTML5 标准,如果
2. 破局者:MessageChannel
为了能实现“高频、低延迟”的控制权交接,React 最终选择利用 MessageChannel 来生成一个干净的宏任务(Macro-task)。
为什么必须是宏任务?
因为微任务(Micro-task,如 Promise.then)会在浏览器当前这一帧的UI 渲染之前全部执行完毕。如果 React 依然用微任务来处理后续的 Fiber 节点,主线程依然会被阻塞,浏览器依然没有机会去渲染画面。
只有将后续任务放进宏任务队列,浏览器才能在执行完当前的宏任务后,去处理用户的点击和页面的重绘,然后再来执行 React 安排的下一个宏任务。
3. 代码模拟:React 是如何“申请” 5ms 的?
下面这段纯 JavaScript 代码是 React Scheduler 核心原理的极简版本。它展示了 React 是如何利用 MessageChannel 和时间戳来实现精准打断的:
1// ==========================================
2// 模拟 React Scheduler 的核心切片逻辑
3// ==========================================
4
5const taskQueue = [/* ...假设这里有 10000 个需要处理的 Fiber 节点... */];
6let isMessageLoopRunning = false;
7
8// React 默认的时间切片大小为 5ms (源码中会根据设备帧率动态调整,但通常是 5ms)
9const yieldInterval = 5;
10
11// 1. 创建一个 MessageChannel
12const channel = new MessageChannel();
13const port = channel.port2;
14
15// 2. 监听消息,这会在下一个宏任务中执行
16channel.port1.onmessage = function performWorkUntilDeadline() {
17 if (taskQueue.length === 0) return;
18
19 // 获取当前时间戳
20 const currentTime = performance.now();
21 // 计算出当前这个“时间片”的截止时间 (当前时间 + 5ms)
22 const deadline = currentTime + yieldInterval;
23
24 // 3. 在截止时间内,拼命执行任务 (同步执行)
25 // 只要没到截止时间,且还有任务,就一直做
26 while (performance.now() < deadline && taskQueue.length > 0) {
27 let task = taskQueue.shift(); // 取出一个 Fiber 节点
28 execute(task); // 执行该节点的工作 (模拟 Diff、打标签等)
29 }
30
31 // 4. 关键点:时间到了 (5ms 用完了),检查任务是否做完
32 if (taskQueue.length > 0) {
33 // 任务没做完,说明被打断了。
34 // 我们必须主动停下来,并通过 postMessage 发送一条消息。
35 // 这会在浏览器的事件循环中插入一个新的宏任务。
36 // 此时,控制权被交还给了浏览器,浏览器可以去处理高优先级的用户点击或渲染动画!
37 port.postMessage(null);
38 } else {
39 // 任务全部处理完毕
40 isMessageLoopRunning = false;
41 }
42};
43
44// 触发组件更新的入口函数
45function scheduleWork() {
46 if (!isMessageLoopRunning) {
47 isMessageLoopRunning = true;
48 // 发送消息,这不会立即执行,而是排队到浏览器的宏任务队列中
49 port.postMessage(null);
50 }
51}
52
53// 模拟执行一个 Fiber 节点的工作
54function execute(task) {
55 // 实际的 React 会在这里做新老 Virtual DOM 对比等计算
56}总结整个流程
- React 开始
Render阶段,调用performance.now()记下当前时间。 - React 开始在一个
while循环中处理 Fiber 节点。每处理完一个节点,就用performance.now()检查一下逝去的时间。 - 【主动申请结束】 如果发现从开始到现在已经超过了 5ms,React 就会立刻跳出
while循环。 - React 调用
port.postMessage(null),告诉浏览器:“我先不干了,我把下一个任务放到你的宏任务队列里排队。” - 浏览器接管主线程,发现有 CSS 动画要渲染、有按钮点击事件要处理,于是顺畅地执行了这些操作。
- 浏览器忙完自己的事,从宏任务队列里拿出了 React 刚才放进去的
onmessage任务,继续从上一次暂停的 Fiber 节点开始下一个 5ms 的处理。
这就是 React Fiber “让出控制权”与“切片”的底层魔法。它不是向浏览器发出了什么特殊指令,而是巧妙地利用了 JavaScript 事件循环(Event Loop)机制和高精度时间戳。