React 的公开模型是 UI = f(state):你描述界面在给定 state 下应长什么样子,当 state 改变时,React 更新输出。
一次 render 不是「React 调用 component 然后更新 DOM」。那个描述跳过了 stale closures、state 在 list items 之间跳动、开发时 Effect 跑两次,以及被放弃的 transitions 背后的机制。
React 维护自己的界面表示。一次 update 被 scheduled,React 在 render phase 计算可能的下一棵树,然后才在 commit phase 发布结果。
event, promise, or external store
→ enqueue an update
→ assign a priority
→ render and reconcile a possible next tree
→ commit the finished changes
→ let the browser paintRender purity、state snapshots、以 type 与 key 决定 identity,以及 render/commit 分割,属于公开模型。像 Fiber、lanes、beginWork 这类名称描述的是 React 19.2 的实现。应用代码不应依赖它们。
Fiber 的走访与可中断 rendering,见 Beyond The DOM 的 React Fiber 与 Concurrent Rendering in React 18。
1. JSX 产生的是描述,不是 DOM Nodes
JSX 是描述 UI 的语法。Transform 会把它转成 React element——一份描述,不是 DOM node,也不是 Fiber。
function Greeting({ name }: { name: string }) {
return <h1 className="greeting">Hello, {name}</h1>
}{
type: "h1",
key: null,
props: {
className: "greeting",
children: ["Hello, ", name],
},
}- Elements 是便宜、immutable 的 snapshots。建立一个并不会 mount 任何东西、跑 Effect,或碰 DOM。
- Component element 的
type是 function。React 稍后在 rendering 时调用它。 - 直接调用 component——
Greeting({ name: "Ada" })——会绕过 identity 与 Hook bookkeeping。 - React 可能为一次永远不会 commit 的 render 建立并检查许多 elements。
要分开看待三棵树:
React elements declarative descriptions returned by components
Fiber tree React's persistent bookkeeping and unit-of-work tree
Host tree DOM nodes, native views, or another renderer's output- Host tree 取决于 renderer:
react-dom、React Native、React Three Fiber。React 本身对 renderer 无关。 - 一个新的 element object 不代表一个新的 DOM node。一次 component render 不代表一次可见的 browser update。
- React Native 如何通过 Expo 做这件事,见 深入理解 React Native。
2. Fiber 是 React 的 Unit of Work
Elements 是暂时的描述。React 需要一个能从一次 render 存活到下一次的持久结构:Fiber tree。
- 一个 Fiber 是某个 component、host element、text node 或 Suspense boundary 的 virtual stack frame。Host nodes 也有 Fiber——不只 function components。
- Fibers 使用
child、sibling与return链接。那是 linked list,不是 children 数组。下一个 unit of work 永远是跟着 pointer 走。 - 旧的 stack reconciler 用 JavaScript call stack 的递归走树,无法暂停。Fiber 用 work loop 走,所以 React 可以在 units 之间停下,再从存好的 pointer 继续。
- Current Fiber 是屏幕上的那棵树。它的
alternate是 work-in-progress Fiber,React 在那里计算下一状态。 - 这是 double buffering:私下突变 WIP,保持 current tree 与 DOM 稳定。放弃工作靠丢掉 WIP,而不是突变屏幕。
- Fibers 与 subtrees 可以重用。React 不会从头克隆整个应用。
{
type, // function, host tag, or other Fiber kind
child, sibling, return,
memoizedProps,
memoizedState,
alternate, // the other buffer
}App
child → Header
sibling → Main
child → Article
Article.return → Main
Main.return → Appcurrent tree work-in-progress tree
App ───────────── alternate ───────────── App
└─ List ──────── alternate ──────────── List
└─ Item ──── alternate ──────────── Item3. State Setter 会 Schedule 工作
调用 state setter 并不会突变正在执行的 function 所捕捉的 state 变量。它为未来的 render queue 一个值。
function Counter() {
const [count, setCount] = useState(0)
function increment() {
setCount(count + 1)
console.log(count) // Still 0 in this event handler
}
return <button onClick={increment}>{count}</button>
}countbinding 属于某一次 invocation。Handler 关闭的是那次 render 的 snapshot。- 可观察模型:
current state snapshot + queued updates → next state snapshot。 - React batches updates,以便在一组相关 setters 之后只 render 一次。Batching 不是把每次 update 合并成一个值。
- 当下一个值等于当前值(
Object.is)时 bail out,是优化,不是保证。
当几次 updates 依赖前一次结果时,updater functions 很重要:
function addThree() {
setCount((value) => value + 1)
setCount((value) => value + 1)
setCount((value) => value + 1)
}Failure: 三次 setCount(count + 1) 都捕捉同一个 count,都请求 1。它们不会形成一串 +1 操作。
4. Lanes 代表 Priority
不是每次 update 都同样紧急。打字应该立刻有感觉;一份大的 search result 可以等。
React 19.2 用 lanes 表示 priority——bitmask 里的 bits,让几类 pending 工作可以同时存在。
urgent input update ─┐
default data update ├─ pending lanes on the root
transition update ┤
retry for Suspense ─┘一次 click 通常占的是 SyncLane——urgent-input 这类工作在实现里的名字,不是应用代码会 import 的东西。
一次很长的 blocking render 会占住 main thread:React 结束之前,browser 不能 paint、处理 click,或滚动。那正是 Fiber 与 lanes 要避免的冻结——但只对 opt-in 的 renders 成立。
Concurrent rendering 是 按 feature opt-in。默认的 setState 仍是一次不中断的 transaction。startTransition、useTransition、useDeferredValue,以及相关的 Suspense 行为,才标记 React 可以中断的工作。
Concurrent render 期间,scheduler 可以在 Fibers 之间 yield(实现里的 shouldYield——大约几毫秒的 time slice,不是 API)。React 把控制权交回 browser,再从同一棵 work-in-progress tree 继续。若更高 priority 的 update 到来,它可以丢掉这份 WIP 并重启。
- React 可能开始一次 transition,收到 urgent input,丢掉这次 transition 尝试,commit urgent 结果,再对着最新的 tree 重启 transition。
startTransition把该 function 里同步 schedule 的 React updates 标成 transition priority。- 它不会把 JavaScript 搬到另一条 thread、让昂贵计算变免费,或延迟传给
startTransition的那个 function。 useTransition是同样的标记,外加isPending,让你可以显示 pending UI,不必藏起 上一次已 commit 的树。useDeferredValue是你并未亲自 schedule 的那个值的对应做法:先继续显示旧值,让较重的 derived render 追上来。- Lane 名称与 yield 预算可能随版本改变。耐用的课是:React 分开追踪 哪些工作 pending,以及 现在该尝试哪些工作。
function handleChange(value: string) {
setQuery(value)
startTransition(() => {
setFilter(value)
})
}同一套 urgent/transition 拆分就是 counter 与 heavy chart demo:increment 保持 urgent;把 setSeed 包进 startTransition,chart 就可以 yield。
const [isPending, startTransition] = useTransition()
function regenerate() {
startTransition(() => {
setSeed(nextSeed())
})
}
return (
<button onClick={regenerate} disabled={isPending}>
{isPending ? "Updating…" : "Regenerate"}
</button>
)5. Render Phase 计算候选树
Rendering 回答的是:「若套用这些 updates,树应该长什么样?」
while next unit of work:
beginWork(fiber) // descend: invoke component, reconcile children
→ if child exists, that child is next
→ else completeWork(fiber) // ascend: prepare host, bubble effect flags
then sibling, or return to parent
concurrent and shouldYield? pause; resume this pointer later走访顺序仍是 depth-first。与递归 call stack 的差别是:这个 loop 把下一个 Fiber 存在变量里,所以可以在 units 之间停下。
beginWork处理一个 Fiber 的 inputs。对 function component,这是 React 调用它、跑 Hooks,然后 reconcile 返回的 children 的地方。对 host element,它 reconcile children,不调用 component function。completeWork在 descendants 完成后跑。对 host components,React 可以建立或准备 DOM instances,并把 effect flags 往 root 冒泡。这里不突变 DOM。- Render phase 必须是 pure,因为它是 speculative。Concurrent render 可能 yield、重启、render 不止一次,或在不 commit 的情况下丢掉结果。默认的 blocking render 仍会把这个 loop 从头跑完。
- Rendering 可以读 props、state、context 与 immutable snapshots。它可以计算并返回 elements。
- 它不得发送 analytics、突变共享 objects、subscribe,或编辑 DOM。
let nextId = 0
function Row() {
nextId += 1
return <li>Row {nextId}</li>
}Failure: 可见的 IDs 现在取决于 React 碰巧调用 Row 多少次,包括开发时检查与被放弃的 renders。
6. Reconciliation 决定 Identity
React 用 type、position 与 key,把新的 child elements 对上现有 Fibers。
对同一位置的 child:
- 相同 type 且 key 兼容:重用 Fiber 并保留其 state。
- 不同 type:unmount 旧 subtree 并 mount 一个新的。
- 不同 key:即使 type 相同,也是新的 identity。
function Profile({ editing }: { editing: boolean }) {
return editing ? <Editor /> : <Preview />
}Editor 与 Preview 占同一位置但 type 不同,所以切换模式会重置 subtree 的 state。
Key 可以刻意重置 state。<Editor key={documentId} /> 通常比用 Effect 清掉每个 field 更干净。
{
todos.map((todo) => <TodoRow key={todo.id} todo={todo} />)
}当一个 item 被移除,React 用新的 element list 对上 current Fibers:
keys in the new list: 1, 3
current sibling list: 1 → 2 → 3
key 1 matches → reuse / clone that Fiber
key 2 is gone → mark a deletion flag (no DOM removal yet)
key 3 matches → reuse / clone that Fiber缺少的 Fiber 在 render 期间只被 标记。Host node 在 commit phase 才删除。这让 render phase 保持 pure 且可中断。
- Keys 只需要在 siblings 之间唯一。它们不是普通 prop。
- State 并不「活在 component function 里面」。React 把它关联到 parent tree 某个位置上 Fiber 的 identity。
Failure: 数组 index 只有在 identity 真的跟随 position 时才安全。若 reorder 改变了谁拥有 index 0,React 可能把 state 留在那个 index,并挂到错误的 data 上。
7. Commit Phase 发布结果
Render 完成时,React 有一棵 Fiber tree,以及描述必要变更的 flags。对一个 root 来说,committing 是 synchronous。React 不会展示一次 commit 的一半与另一次的一半。
before-mutation work
→ DOM mutations and deletions
→ current tree switches to the finished tree
→ refs and layout effects
→ browser gets an opportunity to paint
→ passive effectsgetSnapshotBeforeUpdate在 mutations 之前立刻读取 host 信息。- Ref callbacks 与
useLayoutEffect在 paint 之前观察已 commit 的 DOM。Layout 工作会挡住 paint——留给必须在用户看到这一帧之前完成的测量。 useEffect是 passive,通常在 paint 之后跑。- React 再次跑一个 Effect 之前,会先跑上一次的 cleanup。Unmount 时跑最后一次 cleanup。
- DOM 改变与 pixels 出现不是同一件事。一次很长的 layout Effect 可以延迟一次 commit 的 paint,即使 DOM 已经更新。
useEffect(() => {
const connection = connect(roomId)
connection.open()
return () => connection.close()
}, [roomId])8. Hooks 是 Fiber 上的 Positional State
Hook state 由 React 存储,不在本地 JavaScript 变量里。Function component Fiber 把 Hooks 存成 linked list。Dispatcher 按 调用顺序 走那条 list。
Profile fiber
memoizedState
→ useState hook
→ useReducer hook
→ useEffect hook
→ null- 第一个
useState拿到第一个 state cell。第二个 Hook 拿到第二个 cell。 - 「只在 top level 调用 Hooks」是为了保住这个序列。条件应放在 Hook 里面。
- 每个 state Hook 拥有一条 update queue。较低 priority 的 updates 可以继续 queued,稍后再 replay。
- 每次 render 都会建立新的 handlers,捕捉那次 render 的 props 与 state。「Stale closure」就是较旧 render 的代码稍后才跑。
- 省略一个 reactive dependency 并不会把它冻在最新值——它会保住较旧的 closure。
useEffect(() => {
const id = setInterval(() => {
console.log(count)
}, 1000)
return () => clearInterval(id)
}, []) // This closure keeps the initial count.Failure: 若一个 Hook 坐在 if (editable) 后面,当 editable 改变时,它之后的每个 Hook 都会 移位,React 就无法再对上 cells。
9. Bailouts 与 Memoization 跳过工作,不是跳过意义
当 props、state、context 与 lanes 显示它及其 descendants 在当前 priority 不需要工作时,一个 Fiber 可以 bail out。
memo在 component boundary 加上 props 比较。useMemo在 committed renders 之间缓存一次计算,只要 dependencies 保持相等。useCallback对 function reference 做同样的事。- 这些是性能工具,不是语义保证。若 React 重新计算,结果仍然必须正确。
- Parent 可以 render 而 child 不 render。Component 可以 render 而 DOM 不变。
Runtime 决定一个 Fiber 能否 bail out。React Compiler Internals 是 build-time 那一半:HIR、effects 与 reactive scopes 如何自动产生更细的 cache boundaries。
调查性能时,把三个问题分开:
- 是否有 update 被 scheduled?
- 哪些 components 做了 render 工作?
- 哪些 host 变更与 Effects 被 committed?
10. Suspense 把等待变成 Render Control Flow
Suspense 让一次 render 可以说树的一部分还没准备好。在 React 19 里,use 可以读一个 Promise。
function UserProfile({
userPromise,
}: {
userPromise: Promise<{ name: string }>
}) {
const user = use(userPromise)
return <h2>{user.name}</h2>
}
function Page({ userPromise }: { userPromise: Promise<{ name: string }> }) {
return (
<Suspense fallback={<p>Loading profile...</p>}>
<UserProfile userPromise={userPromise} />
</Suspense>
)
}- 若 Promise 仍 pending,React 会 suspend 那条路径,找到最近的 Suspense boundary,并可以 render 它的 fallback。
- Suspense 不会 fetch。Framework 或 cache 必须提供一个 stable Promise。
- 等待属于 candidate tree。Committed tree 不必因为另一次尚未完成的 render 而消失。
- 对非紧急 navigation,React 可以让已经可见的内容留在屏幕上,同时准备下一状态。
Failure: 每次 client render 都建立一个新的 Promise,可能反复 suspend,因为每次 render 看到的都是新的 resource。
11. Hydration 重用 Server HTML
Server rendering 产生 HTML,但 HTML 不含 React state、event handling,或 client Fiber tree。Hydration 在匹配现有 host nodes 的同时,建立 client 表示。
server
React tree → HTML stream → browser displays content
client
load JavaScript
→ create root work
→ match Fibers to existing DOM
→ attach event behavior
→ continue as an interactive React tree- Hydration 不是「加上 event listeners」。React 必须重建 state 与 context、把 Fibers 关联到 host nodes,并验证 markup 兼容。
- Mismatch 表示 server 与 client 对同一棵初始树产生了不同描述——时钟、随机值、仅 browser 的分支、locale 差异、无效的 HTML nesting。
- 让初始 render 可确定,传入 server snapshot,或在 hydration 之后再 render 仅 client 的信息。
suppressHydrationWarning是一层的逃生口,不是通用修复。- Suspense boundaries 让 React 分段 hydrate,整页不必等一次全有或全无的 render。
12. Strict Mode 测试可重启的代码
在开发时,Strict Mode 可能把 render functions 调用两次、为 Effects 多跑一轮 setup-and-cleanup,并重跑 ref callbacks。这些检查在 production 不会以同样方式发生。
- 目的是找出在 rendering 被重启、或 synchronization 被 remount 时会坏掉的代码。
- 会突变 props 的 impure render,会在那次额外 render 上暴露自己。
- 没有 cleanup 的 Effect 会暴露重复 subscriptions。解法是一次 cleanup,让 setup → cleanup → setup 等价于一次真正的 setup。
- Render 期间纯、Effect cleanup 对称的代码,不必在意 React 做了多少次 speculative 尝试。
13. 端到端跟一次 Update
没有 concurrent feature 时,一次沉重的 setState 会在 main thread 上把 work loop 跑完。页面在那次 render 结束前无法 paint,也接不了下一次按键。
用户在 search field 打字。应用紧急更新 input,并在 transition 里过滤一份大 list。
- Handler 调用
setQuery(urgent)以及startTransition+setFilter(transition)。 - React 先选 urgent 工作,render、reconcile,并 commit input DOM。
- Browser 可以 paint 最新的 input 值。
- React 以可中断的 slices 尝试这次 transition。若又有一次按键到来,它可以丢掉这份 WIP,commit 更新的 urgent input,再对着最新的 tree 重启 filter。
- 当一次 transition render 完成,React commit 过滤后的 list。Passive Effects 随后跑。
- React 从不突变较旧 handler 里的
querybinding。它用新的 snapshots 建立新的 renders。 - 部分完成的 transition 永远不会漏进 DOM。只有完成的 commit 才会改变可见的 host tree。即使
isPending为 true,上一次已 commit 的 UI 仍留在屏幕上。
最短且准确的模型:
Elements describe.
Fibers remember.
Queues collect.
Lanes prioritize.
Rendering computes.
Reconciliation matches.
Committing publishes.
Effects synchronize.当 React 行为让你意外时:
- 哪个 render 建立了这个 callback? 它的 closure 包含那次 render 的 props 与 state。
- Setter 拿到的是 replacement 还是 updater? 重复的 replacements 可能都用同一份 snapshot。
- 这个 component 的 identity 是什么? Parent position、type 与 key。
- 这段代码是在 render 还是 commit 期间跑? Render 必须是 pure。
- React 是 render 了、commit 了,还是两者都做了? Component 里的 console log 并不能证明 DOM 变了。
- 这次工作可能被重启过吗? Strict Mode 与 concurrent rendering 会把这件事变得可见。
- 所有 reactive Effect dependencies 都声明了吗? 若没有,很可能正在重用较旧的 closure。
常见误解:
- Fiber 不是「virtual DOM」。 Elements 是描述;Fibers 是 reconciler 持久的工作与 state。
- Fiber 是 linked list 上的 loop,不是 JavaScript stack 上的递归。 这才让暂停与继续成为可能。
- Fiber 不会让每次 render 都更快。 它的好处是给工作排优先级,并避免挡住 browser——而且只对 opt-in concurrent rendering 的工作成立。
- Concurrent 不代表 parallel,也不是每个
setState的默认。 React 仍然在 main thread 上协调 JavaScript。Concurrent features 让它可以交错、yield,并丢掉过时的 renders。 - Fiber 不是 React Three Fiber。 一个是 reconciler。另一个是以它命名的 Three.js renderer。
较新的 APIs 见 React 19 新功能。可视化:React Fiber 与 Concurrent Rendering in React 18。主要参考:Andrew Clark 的 React Fiber architecture notes,以及 React 文档中的 render and commit、state as a snapshot、preserving state、Effects、transitions、Suspense 与 hydration。