了解 Fiber 如何协调组件树、批处理更新,并让渲染工作变得可中断。
在 Kodokon 中打开本课自 React 16 起,内部引擎被称为 Fiber:组件树中的每个元素都由一个 fiber 节点表示,也就是一个工作单元。React 实际上维护着两棵树(一种双缓冲技术):反映当前屏幕内容的 current 树,以及渲染过程中构建的 workInProgress 树。工作分为两个截然不同的阶段:渲染阶段(调用你的组件、计算差异),它是可中断且可重放的;然后是提交阶段(把变更应用到 DOM、运行副作用),它始终是同步且不可中断的。协调依赖两条启发式规则,将比较的成本降到线性:如果某个位置上元素的类型发生变化,React 会销毁整棵子树并连同状态一起重建;对于列表,key prop 让 React 能在两次渲染之间匹配元素。
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.第二个基础机制:批处理。当多次状态更新在同一上下文中被触发时,React 会把它们合并成一次渲染。在 React 18 之前,这种合并只在 React 事件处理器内部生效;自 createRoot 起,它在任何地方都是自动的:定时器、Promise、原生监听器。这也是为什么在 setState 之后立即读取状态会返回旧值:更新是被安排的,而不是被立即应用的。
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 变化时强制同步渲染来解决这个问题。
import { useSyncExternalStore } from 'react';
function useWindowWidth() {
return useSyncExternalStore(
(notify) => {
window.addEventListener('resize', notify);
return () =>
window.removeEventListener('resize', notify);
},
() => window.innerWidth
);
}<div> 元素变成了 <span>,React 会怎么做?createRoot 下,setTimeout 内部的两次 setState 调用会触发多少次渲染?