Skip to content

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 树,alternate 为 null,baseState 为 0

  • fiber 上的 memoizedState 中 baseState = 0,即初始化 useState 的值;
  • fiber 上的 alternate 为 null;
  • Index 组件上的 number 为 0。

初始化流程:组件第一次初始化时会调和渲染形成一棵 fiber 树(简称为树 A),树 A 的 alternate 属性为 null。

控制台打印的 fiber 对象,展开 memoizedState 可见 baseState 的取值

2. 第一次点击 setNumber(1) ​

  • 树 A 上的 memoizedState 中 baseState = 0;
  • 树 A 上的 alternate 指向另外一个 fiber(这里称之为树 B);
  • Index 组件上的 number 为 1。

展开 alternate 查看树 B 的信息,会发现树 B 上 memoizedState 的 baseState = 1。

展开 alternate 后的树 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) ​

第二次点击后 current 树的 memoizedState 中 baseState 为 1

  • 树 A 上的 memoizedState 中 baseState 仍为 0;
  • 树 B 上的 baseState 为 1;
  • Index 组件上的 number 依然为 1,但函数组件又被执行了一次。

第二次点击之后,树 A 和树 B 都更新到最新的 baseState = 1,此时再点击按钮才真正跳过渲染。

四、为什么相同的 state 会渲染两次 ​

双缓存:在内存中构建、构建完成后直接替换的技术,用两棵树交替充当「正在展示」和「正在构建」的角色,避免渲染过程中改动当前显示的内容。

双缓存全流程:初始化、第一次点击、第二次点击后两棵 fiber 树的 alternate 与 baseState 变化

更新机制:在一次更新中,首先会获取 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 中的双缓存