Kodokon kodokon.com

深入底层:协调、批处理、并发渲染

了解 Fiber 如何协调组件树、批处理更新,并让渲染工作变得可中断。

11 分钟 · 3 题

在 Kodokon 中打开本课

自 React 16 起,内部引擎被称为 Fiber:组件树中的每个元素都由一个 fiber 节点表示,也就是一个工作单元。React 实际上维护着两棵树(一种双缓冲技术):反映当前屏幕内容的 current 树,以及渲染过程中构建的 workInProgress 树。工作分为两个截然不同的阶段:渲染阶段(调用你的组件、计算差异),它是可中断且可重放的;然后是提交阶段(把变更应用到 DOM、运行副作用),它始终是同步且不可中断的。协调依赖两条启发式规则,将比较的成本降到线性:如果某个位置上元素的类型发生变化,React 会销毁整棵子树并连同状态一起重建;对于列表,key prop 让 React 能在两次渲染之间匹配元素。

JSX
function List({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <input defaultValue={item.label} />
        </li>
      ))}
    </ul>
  );
}
// With key={index}, deleting the first item
// would shift the content of the remaining inputs.
稳定的 key 能保留每一行的状态和 DOM。

第二个基础机制:批处理。当多次状态更新在同一上下文中被触发时,React 会把它们合并成一次渲染。在 React 18 之前,这种合并只在 React 事件处理器内部生效;自 createRoot 起,它在任何地方都是自动的:定时器、Promise、原生监听器。这也是为什么在 setState 之后立即读取状态会返回旧值:更新是被安排的,而不是被立即应用的。

JSX
function handleTick() {
  setCount((c) => c + 1);
  setFlag((f) => !f);
  // React 18: a single render, even here.
}

setTimeout(handleTick, 100);
// Before React 18, this timer would trigger
// two successive renders.
自动批处理现在覆盖了所有上下文。

并发渲染把这套逻辑推得更远:渲染阶段可以被中断、恢复,甚至被丢弃并重新开始,如果有更紧急的更新进来的话。React 按优先级(即 lanes)对更新进行排序:一次按键是紧急的,而用 startTransition 标记的列表筛选则不是。直接的后果是:你的组件函数必须是纯的,因为 React 可能会多次调用它们却从不提交结果。另一个边缘情况:在可中断渲染期间读取的外部 store(Redux、Zustand,或自制的单例)可能会在渲染中途发生变化,从而产生一个不一致的界面,这被称为撕裂(tearing)useSyncExternalStore API 通过在 store 变化时强制同步渲染来解决这个问题。

JAVASCRIPT
import { useSyncExternalStore } from 'react';

function useWindowWidth() {
  return useSyncExternalStore(
    (notify) => {
      window.addEventListener('resize', notify);
      return () =>
        window.removeEventListener('resize', notify);
    },
    () => window.innerWidth
  );
}
读取外部 store 而没有撕裂的风险。

知识检测

确认你已牢记本课的重点内容。

  1. 在协调期间,如果同一位置上的 <div> 元素变成了 <span>,React 会怎么做?
    • 它更新标签并保留子元素
    • 它连同状态一起销毁整棵子树,然后重建它
    • 它先递归比较子元素再做决定
    • 它抛出一个协调错误
  2. 在 React 18 和 createRoot 下,setTimeout 内部的两次 setState 调用会触发多少次渲染?
    • 两次,因为批处理只适用于 React 事件
    • 只有一次,得益于自动批处理
    • 零次,事件之外的更新会被忽略
  3. 在并发模式下,React 工作的哪个阶段是可中断的?
    • 仅提交阶段
    • 仅渲染阶段
    • 两个阶段都是
    • 都不是,React 保持完全同步