主题
React Fiber 双缓冲机制
通过一个连续两次
setNumber(1)的实验,观察 Fiber 树alternate的切换过程,解释 React 为何在 state 未变化时仍会再渲染一次,以及createWorkInProgress在更新中的作用。
可视化工具:双缓存可视化。
一、现象:设置相同 state 仍渲染两次
useState 源码中对 state 做了浅比较,值相等时不会触发组件渲染。但在下面的例子中,连续点击按钮,组件却渲染了两次:
jsx
import React, { useState } from "react";
const Index = ({ consoleFiber }) => {
const [number, setNumber] = useState(0);
console.log(number);
consoleFiber(); // 每次 fiber 更新后,打印 fiber 检测 fiber 变化
return (
<div className="page">
<div className="content">
<span>{number}</span>
<br />
<button onClick={() => setNumber(1)}>将 number 设置成 1</button>
<br />
</div>
</div>
);
};
class App extends React.Component {
consoleChildrenFiber() {
console.log(this._reactInternals.child); // 用来打印函数组件 Index 对应的 fiber
}
render() {
return <Index consoleFiber={this.consoleChildrenFiber.bind(this)} />;
}
}
export default App;说明:
_reactInternals是 React 挂在类组件实例上的内部字段,普通业务代码不应依赖它,这里只用于调试时观察 fiber。
二、current 树与 workInProgress 树
React 中最多会同时存在两颗 fiber 树:当前显示内容的 fiber 树称为 current 树,正在内存中构建的 fiber 树称为 workInProgress 树,它们通过 alternate 属性互相连接。
三、逐次点击的观察结果
1. 初始化打印

- fiber 上的
memoizedState中baseState = 0,即初始化useState的值; - fiber 上的
alternate为null; - Index 组件上的 number 为 0。
初始化流程:组件第一次初始化时会调和渲染形成一棵 fiber 树(简称为树 A),树 A 的 alternate 属性为 null。

2. 第一次点击 setNumber(1)
- 树 A 上的
memoizedState中baseState = 0; - 树 A 上的
alternate指向另外一个 fiber(这里称之为树 B); - Index 组件上的 number 为 1。
展开 alternate 查看树 B 的信息,会发现树 B 上 memoizedState 的 baseState = 1。

得出结论:更新的状态都在树 B 上,而树 A 上的 baseState 还是之前的 0。
更新流程:第一次更新渲染时,由于树 A 中不存在 alternate,React 直接复制一份树 A 作为 workInProgress(即树 B),所有更新都在树 B 中进行,所以 baseState 被更新成 1,然后用树 B 进行渲染。渲染结束后树 A 与树 B 通过 alternate 相互指向,树 B 作为下一次操作的 current 树。
3. 第二次点击 setNumber(1)

- 树 A 上的
memoizedState中baseState仍为 0; - 树 B 上的
baseState为 1; - Index 组件上的 number 依然为 1,但函数组件又被执行了一次。
第二次点击之后,树 A 和树 B 都更新到最新的 baseState = 1,此时再点击按钮才真正跳过渲染。
四、为什么相同的 state 会渲染两次
双缓存:在内存中构建、构建完成后直接替换的技术,用两棵树交替充当「正在展示」和「正在构建」的角色,避免渲染过程中改动当前显示的内容。

更新机制:在一次更新中,首先会获取 current 树的 alternate 作为当前的 workInProgress,渲染完毕后 workInProgress 树变为 current 树。Hooks 中的 useState 进行 state 对比时,用的是缓存树上的 state与当前最新的 state:
- 第一次点击后,树 B(current)的
baseState已是 1,但它的alternate(树 A)上仍是 0; - 第二次点击
setNumber(1)时,比较的双方分别是树 A 上的 0 与最新的 1,浅比较不相等,于是又走了一次更新流程; - 这次更新把两棵树的
baseState都对齐到 1,之后再设置相同的值才会命中浅比较、跳过渲染。
这就解释了为什么更新相同的 state,函数组件还是会执行两次。
五、createWorkInProgress
- 函数:
createWorkInProgress - 位置:
packages/react-reconciler/src/ReactFiber.old.js
可以参考的实现思路:优先复用传入的 workInProgress,没有则通过 alternate 复用 current 树上的节点;两者都会重置 pendingProps、expirationTime 等字段,并把 alternate 双向连好。
参考文章:React 中的双缓存