React 很容易被学成一堆 API:
-
useState -
useEffect -
useContext -
memo -
Suspense -
startTransition
但这些 API 背后,其实存在一条非常统一的运行时主线。
如果把这条主线打通,很多 React 机制就不再是孤立知识点。
可以先把整篇文章压缩成一句话:
React 先用 React Element 描述 UI,再用 Fiber 保存运行时状态和工作信息;update 到来后,Reconciler 在 Work In Progress Fiber Tree 上计算下一棵 UI,最后通过 Commit 把变化同步到 DOM。
完整链路:
index.html
↓
mount container
↓
createRoot(container)
↓
root.render(<App />)
↓
React Element
↓
Fiber Tree
↓
Render / Reconciliation
↓
Commit
↓
DOM
↓
Browser Rendering Pipeline
↓
Pixels
下面从头走一遍。
1. React 从哪里开始?Mount Container
一个普通 Vite React 应用,入口通常从 index.html 开始:
<div id="react-root"></div>
<script type="module" src="/src/main.tsx"></script>
浏览器解析 HTML,加载 main.tsx。
例如:
import ReactDOM from 'react-dom/client'
const rootElement = document.getElementById('react-root')!
const root = ReactDOM.createRoot(rootElement)
root.render(<App />)
这里的:
<div id="react-root"></div>
就是 React 的:
mount container
createRoot(container) 可以先理解成:
从这个 DOM 节点开始,这块 UI 交给 React 管理。
所以 React 并不是天然“接管整个 HTML”。
更准确的是:
DOM Tree
<body>
├── header
├── #react-root ← React 管这里
└── footer
React 管理的是这个 container 下面的 DOM subtree。
2. DOM Tree 和 Component Tree 不是一个东西
假设:
root.render(
<StrictMode>
<JotaiProvider>
<QueryProvider>
<RouterProvider />
</QueryProvider>
</JotaiProvider>
</StrictMode>,
)
React 看到的是:
StrictMode
└── JotaiProvider
└── QueryProvider
└── RouterProvider
└── ...
这是一棵:
React Component Tree
最终浏览器看到的则是:
DOM Tree
所以要先分清两个世界:
Component Tree
↓
React Runtime
↓
DOM Tree
React 的 Context、Suspense、Error Boundary、state identity、reconciliation 等大量规则,首先都是建立在 Component Tree / Fiber Tree 上,而不是浏览器 DOM 层级上。
3. <App /> 到底是什么?
这一点非常关键。
root.render(<App />)
这里并不是先直接执行:
App()
而是 JSX 先变成一个 React Element。
概念上:
<App />
变成类似:
{
type: App,
key: null,
props: {}
}
也就是说:
React Element 是一个 UI description。
它描述:
这里应该有一个 App
它的 props 是这些
它的 key 是这个
然后 React Runtime 才开始处理这个 element。
大致过程:
<App />
↓
React Element(type = App)
↓
创建 / 复用 App Fiber
↓
执行 App()
↓
拿到 App 返回的 JSX
↓
得到新的 React Elements
↓
继续向下
例如:
function App() {
return <Counter />
}
function Counter() {
return <button>Hello</button>
}
可以理解成:
<App />
↓
Element(type = App)
↓
App Fiber
↓
App()
↓
<Counter />
↓
Element(type = Counter)
↓
Counter Fiber
↓
Counter()
↓
<button>Hello</button>
↓
Element(type = "button")
↓
HostComponent Fiber
因此要区分三层东西:
React Element
“我想要什么”
immutable description
↓
Fiber
“React 如何管理这棵树、保存状态、安排工作”
mutable runtime node
↓
DOM
“浏览器真正显示什么”
4. Context:Component Tree 上的一条数据通道
Context 最容易被误解成“全局状态”。
更准确的 mental model 是:
Context 是沿 React Component Tree 向下传播的一条数据通道。
创建:
const UserContext = createContext(null)
这里的 null 不是 initial state。
它更准确是:
default / fallback value
也就是说:
当组件向上找不到对应的 Context Provider 时,才使用这个值。
React 19 可以直接写:
<UserContext value={user}>
<App />
</UserContext>
旧版本常见写法是:
<UserContext.Provider value={user}>
<App />
</UserContext.Provider>
所以:
Context
= 数据通道
Provider
= 在树的某个位置给这条通道提供值
useContext
= 从当前组件向上读取最近的 provider value
例如:
<UserContext value="Alice">
<A />
<UserContext value="Bob">
<B />
</UserContext>
</UserContext>
那么:
A → "Alice"
B → "Bob"
规则就是:
useContext(UserContext)会沿 Component Tree 向上找到最近的同一个 Context Provider。
这描述的是 Context 的空间模型。Context 还有一条时间维度:Provider 的 value 变化后,读取这个 Context 的 consumer 需要被重新处理。
Provider value changed
(Object.is previous / next)
↓
Context invalidation
↓
读取该 Context 的 consumer
↓
重新 render
所以 Context 同时回答两个问题:
读取时找谁?
→ nearest provider
value 变化后谁失效?
→ consumers that read this Context
这也是为什么 memo 不能挡住组件自己的 Context 更新:memo 可以帮助跳过父组件向下传播造成的工作,但 consumer 读取的 Context 发生变化时,它仍然需要重新 render。
所以 Context 的作用域天然就是:
Provider 所包裹的 subtree
并不是所有 Context 都应该放 App 顶层。
原则是:
哪些组件需要共享,就把 Provider 放在它们最近的共同祖先附近。
5. 一个统一的 Tree Mental Model:Nearest Boundary
Context 之后,会发现 React 里还有一批机制也很像:
Context
Suspense
Error Boundary
它们内部实现并不完全相同,但在使用层面的空间模型非常接近:
当前组件发生某类行为时,React 会沿祖先方向寻找最近一个能够处理它的 boundary / scope。
例如:
App
└── Suspense
└── ErrorBoundary
└── UserContext
└── Child
Child:
useContext(UserContext)
→ 找最近的 UserContext value
suspend
→ 找最近的 Suspense boundary
render 抛出可捕获错误
→ 找最近的 Error Boundary
因此可以建立一个很强的 mental model:
React 很多能力,都是在 Component Tree 某个位置建立 scope / boundary,然后影响下面的 subtree。
关键词就是:
ancestor
subtree
scope
boundary
nearest
6. React Runtime 的两个核心 Phase
一次 React 更新,可以先粗略压缩成:
Render Phase
↓
Commit Phase
↓
Browser Rendering Pipeline
Render Phase
Render 阶段主要做:
计算下一棵 UI 应该是什么样。
第一次是 mount:
mount
→ 构建 Fiber Tree
后续是 update。
这里不要把 state / props / context / parent render 放在同一层。更准确要分成三件事:
1. Update Source
setState / dispatch
root.render(...)
Context invalidation
external-store notification
...
↓
schedule work
2. Render Propagation
work 到达某个 Fiber
↓
component 执行,或 bailout
↓
产生新的 React Elements
↓
reconciliation 向 children 继续
3. Render Input
props / state / context
= 当前这次 render 读取的输入
所以:
Update Source
≠ Render Input
≠ Work Propagation
props 通常不是一个独立的 scheduling source;它是 child render 的输入,也是某些 bailout 的判断条件。parent render 也不是根因,它描述的是 work 如何沿树向下传播。
最终仍然是在 WIP Tree 上完成 reconciliation,并计算哪里发生变化。
Render 阶段的重要特点:
可以 pause
可以 resume
可以 restart
可以 abandon
因此 render 必须尽量 pure。
因为 React 有权把一次 render 算一半后丢掉。
Commit Phase
Render 完成之后,React 才进入 commit。
Commit 的职责是:
把计算结果真正作用到 Host Environment。
Web 下就是 DOM。
例如:
textNode.nodeValue = '1'
Commit 不能像 concurrent render 那样随意暂停到一半。
用户应该始终看到一个完整、稳定的已提交世界。
7. React Commit 结束,不代表 Pixels 已经出现
React commit 之后,才进入浏览器自己的 rendering pipeline。
大致:
React Commit
↓
DOM Mutation
↓
Style Calculation
↓
Layout
↓
Paint
↓
Composite
↓
Pixels
并不是每次 DOM mutation 都一定完整经过所有阶段。
例如:
改变 width
→ 可能触发 layout + paint + composite
改变 background
→ 通常不需要 layout
改变 transform
→ 很多时候主要发生 composite
因此 React Runtime Cost 和 Browser Cost 是两个不同问题。
顺便把 Effect 放进这条时间线:
Render
↓
Commit
├── DOM mutation
├── useLayoutEffect(浏览器 repaint 前)
↓
Browser Paint
↓
useEffect(通常在 paint 后;具体时机可因交互和调度而变化)
这里最重要的不是死记精确时序,而是:
Effect 不属于 render。它是在 commit 之后,用来同步 React 与外部系统的。
这也是为什么“先 render,再在 useEffect 里 fetch”天然比 render 前已经知道数据需求的方案更晚启动请求。
8. Parent Render:为什么 Children 会陪跑?
一个很重要的事实是:
默认情况下,父组件 render 后,React 会继续向 children 做 reconciliation,普通 function child 通常也会重新执行。
例如:
function Parent() {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<Child />
</>
)
}
function Child() {
console.log('Child render')
return <div>Hello</div>
}
count 只属于 Parent。
但更新后仍然会发生:
setCount()
↓
Parent render
↓
生成新的 children elements
↓
reconciliation 向下
↓
Child render
这里真正的 update source 是:
Parent 的 setCount()
Child 自己没有 state update,props 甚至可以完全没变,但 Parent render 后,work 默认仍会沿 subtree 向下传播,所以 Child() 可能重新执行。
把几个概念分开:
setCount()
= update source
props
= render input
parent render
= work propagation
memo / bailout
= 截断传播的优化机会
所以“某个组件执行了”并不等于“这个组件自己的 props / state / context 一定发生了变化”。
9. memo:不是“render 了但没执行”,而是 Bailout
例如:
const Child = memo(function Child() {
return <div>Hello</div>
})
当 Parent render 后,Reconciler 来到 Child Fiber,可以先用这个 mental model:
Parent render
↓
来到 memoized Child
↓
props 相同
+ Child 没有自己的 relevant state update
+ 没有 Context invalidation
↓
Child() 可以 bailout,不执行
但:
Child 自己 bailout
≠ subtree 一定完全没有 work
如果 descendants 还有当前 render lanes 相关的 pending work,React 仍然需要进入 subtree。Fiber 上的 childLanes 就服务于这类判断。
所以更完整是:
Child 本身可 bailout
+
subtree 没有 relevant child work
↓
整段 subtree 才能继续跳过
所以要区分:
update 被 schedule
≠ component 一定执行
≠ DOM 一定变化
≠ browser 一定 repaint
三层也不要混:
Component Render
function component 被执行
Reconciliation
React 处理 / 比较 Fiber Tree
Commit
真正修改 Host Environment
优化的本质之一,就是在某个 Fiber 上尽早 bailout,截断不必要的向下工作。
React Compiler 可以自动做一部分等价的 memoization,但 runtime 层面的核心概念没有变:是否能在某个 Fiber 上安全 bailout。
10. Reconciliation:React 怎么判断“还是不是原来的那个组件”?
每次组件执行会得到新的 React Elements。
Reconciler 做的事情之一,就是拿:
旧 Fiber Tree
vs
新 React Elements
进行匹配。
这里不要把 component identity 机械理解成一个固定公式:
type + key + position
更准确的 mental model 是:
React 在当前 tree position / sibling matching context 中
尝试把新的 React Element
匹配到旧 Fiber
其中最重要的显式信号是:
- type
- key
position 更像“在哪个树结构 / sibling 集合里进行匹配”的上下文,而不是 Fiber 上一个独立的 identity 字段。
例如:
<Counter />
Element 大致:
{
type: Counter,
key: null,
props: {}
}
如果下一次同一个位置仍然是:
<Counter />
并且在当前 reconciliation context 中 type / key 能匹配,React 就可以复用原来的 Fiber identity。
对于 keyed list,即使 sibling 的数组位置发生移动,稳定 key 仍然可以帮助 React 在同一组 siblings 中匹配回原来的 identity。
于是:
旧 Fiber
→ 继续服务新的 React Element
→ state 得以保留
如果:
{
show ? <Counter /> : <Profile />
}
同一位置从:
Counter
变成:
Profile
那么 type 不同:
旧 Counter unmount
↓
创建 Profile
↓
Counter state 消失
11. key 的本质是 Identity
key 绝对不只是“为了消除 map warning”。
例如:
<Counter key="A" />
变成:
<Counter key="B" />
即使 type 都是 Counter,React 仍然会认为:
这是两个不同 identity
于是:
A unmount
B mount
state reset
所以:
key 是开发者显式参与 component identity 判定的一种方式。
没有 key 的 sibling,React 更多依赖它们在 sibling list 中的位置来匹配。
这也是列表插入、删除、排序时,稳定 key 极其重要的原因。
12. State 不是“存在函数里”,而是挂在 Fiber Identity 上
例如:
function Counter() {
const [count, setCount] = useState(0)
return <button>{count}</button>
}
Counter() 每次 render 都会重新执行。
既然函数重新执行,那么 count 为什么不会每次重新变成 0?
因为:
state 并不是保存在这次函数调用栈里,而是保存在对应 Fiber 的运行时状态中。
对于 function component,Fiber 的:
memoizedState
通常会指向 Hook 链表。
例如:
function Example() {
const [count] = useState(0)
const [name] = useState('A')
const ref = useRef(null)
}
可以脑补成:
Example Fiber
│
└── memoizedState
↓
Hook #1
useState(count)
↓ next
Hook #2
useState(name)
↓ next
Hook #3
useRef(...)
↓
null
Hook 节点概念上会保存:
memoizedState
baseState
baseQueue
queue
next
具体字段属于 React 内部实现细节,但这个模型非常重要。
13. 为什么大多数 Hooks 不能写在 if 里?
因为 Hook identity 依赖调用顺序。
第一次:
Hook #1 = useState(count)
Hook #2 = useState(user)
Hook #3 = useEffect(...)
如果下一次:
if (loggedIn) {
useState(user)
}
条件变成 false,那么调用顺序可能变成:
Hook #1 = useState(count)
Hook #2 = useEffect(...)
但旧 Fiber 的 Hook #2 明明是:
useState(user)
映射直接错位。
所以 Rules of Hooks 背后并不神秘:
React 需要通过稳定的 Hook 调用顺序,遍历 Fiber 上对应的 Hook 链。
这里有一个 React 19 的特殊例外:use(resource) 不是普通 Hook 的调用模型,它可以出现在条件和循环里。
if (show) {
const theme = use(ThemeContext)
}
但 useState、useEffect、useContext、useMemo 等普通 Hooks 仍然必须保持稳定的调用顺序。
14. setState 不是“直接改变量”
例如:
setCount((c) => c + 1)
它并不是简单做:
fiber.memoizedState = 1
更接近:
创建 Update
↓
放进当前 Hook 的 update queue
↓
给 update 分配 Lane
↓
标记 Fiber / Root 有 pending work
↓
schedule
所以下面代码:
setCount(count + 1)
console.log(count)
console.log(count) 仍然看到当前这次 render 的旧值。
因为:
setState是 enqueue update + schedule render,而不是修改当前函数里的局部变量。
另外,setter 被调用也不等于一定完成一次正常 rerender。若 next state 与 current state 通过 Object.is 判断相同,React 可以跳过后续更新工作。
所以仍然要保持:
setter called
≠ component 一定执行
≠ DOM 一定变化
15. Fiber:React Runtime 的核心数据单元
可以把 Fiber 理解成:
React Runtime 里的核心工作单元 + 状态记录。
一个 Fiber 概念上会保存:
Fiber
├── type / key
├── return
├── child
├── sibling
├── pendingProps
├── memoizedProps
├── memoizedState
├── updateQueue
├── flags
├── subtreeFlags
├── lanes
└── alternate
于是前面很多概念都能回到 Fiber:
Component Tree
→ Fiber Tree
State
→ Fiber.memoizedState
setState
→ Hook update queue
Re-render
→ schedule work on Fiber / Root
Reconciliation
→ 新 Element 与旧 Fiber 匹配
memo
→ bailout
Commit
→ 根据 Fiber flags 修改 Host Environment
mount / update / unmount
→ Fiber identity 是否能继续复用
但要记住:
Fiber 及其字段是 React 内部实现细节,不应该被业务代码依赖。
这里研究它,是为了建立 runtime mental model。
还要再加一条边界:不是应用里的所有数据都存进 Fiber。
React-owned Runtime State
useState / useReducer / Hook state / Context dependencies
│
│ React Runtime
│
├─────────────────────
│
External World
query cache / Redux-like store / browser API / WebSocket / server state
例如 Query Provider、Jotai Provider 出现在 Component Tree 里,并不代表它们管理的所有数据本身都“存进 Fiber”。很多库的数据实体实际存在 React 外部,Provider 只是把 client / store / scope 接进 React Tree。
对于会变化的 external store,React 需要一个一致的订阅入口。标准 primitive 是:
useSyncExternalStore(subscribe, getSnapshot)
它表达的是:
External Store changed
↓
React receives notification
↓
read snapshot
↓
schedule relevant consumer work
这和前面的 Fiber 模型并不冲突:外部数据不属于 Fiber,但外部变化最终仍要转成 React 能调度的 work。
16. Current Tree 与 Work In Progress Tree
React 不会在 render phase 里直接把当前稳定 Fiber Tree 改得面目全非。
它维护一个非常像 double buffering 的模型:
Current Tree
↕ alternate
Work In Progress Tree
current:
上一次已经 commit 的稳定世界。
workInProgress:
render phase 正在内存里计算的下一版本。
同一个逻辑节点的两份 Fiber 通过:
alternate
互相连接。
但不是通过修改 alternate 来完成切换。
真正决定“谁是 current”的,是 root:
root.current
一次更新:
A = current
B = workInProgress
render B
↓
commit B
↓
root.current = B
下一轮:
A 又可以被复用为新的 WIP
所以概念上:
A current
B WIP
commit
↓
B current
A future WIP
这就是 double buffering。
17. 为什么 Concurrent Render 可以被暂停甚至丢弃?
因为用户当前看到的世界仍然由:
root.current
表示。
WIP 只是:
Potential Next UI
所以 render 可以:
算到一半
→ yield
更高优先级更新来了
→ 先做更高优工作
当前结果过时
→ abandon WIP
之后
→ restart / resume
但 current 仍然是上一次完整 commit 的稳定 UI。
这也是:
Render 可以不稳定地计算,但 Commit 必须稳定地提交。
18. Lane:React 如何给 Update 分类和排序
每个 update 并不是简单只有:
“我要改什么”
它还需要表达:
“这件事有多急 / 属于哪类 work”
React 内部使用 Lane。
可以先理解成:
Lane 是 Reconciler 内部用 bitmask 表示 update / work 类别和优先级集合的系统。
例如多个 lane 可以通过位运算组合:
00000001
00000100
00100000
--------
00100101
于是 React 可以快速表示:
哪些 lane pending
哪些 lane suspended
哪些 lane blocked
哪些 lane 要优先 render
update 会携带 lane。
这些 lane 又会从具体 Fiber 向上汇总到 Root。
19. Lane 和 Scheduler Priority 不是同一个东西
这两个概念很容易混。
可以粗略分成两层:
Lane
= React Reconciler 内部的 work 分类 / 优先级集合
Scheduler Priority
= Scheduler 如何安排 CPU 时间执行 work
概念链:
Update
↓
Lane
“这是什么类型 / 优先级的 React work?”
↓
Reconciler 选择 render 哪些 lanes
↓
Scheduler
“什么时候给这坨 work CPU 时间?”
所以:
Lane 与 Scheduler Priority 会协作,但不是同一个 abstraction。
20. 一次 setState() 到底发生了什么?
拿最简单的 Counter:
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}
用户点击一次按钮。
整条链可以压缩成:
browser click
↓
React event handler
↓
setCount(...)
↓
创建 Update
↓
assign Lane
↓
enqueue 到 Hook.queue
↓
mark Fiber / Root pending
↓
schedule
↓
选择 renderLanes
↓
render WIP tree
↓
执行 Counter()
↓
处理 Hook update queue
↓
new state
↓
new React Elements
↓
reconciliation
↓
产生 flags
↓
finishedWork
↓
commit DOM mutation
↓
WIP → current
↓
browser rendering pipeline
下面拆开。
21. Update 进入 Hook Queue
setCount(c => c + 1) 会创建一个 update。
概念上:
Update
├── action: c => c + 1
└── lane: ...
然后进入:
Counter Fiber
└── memoizedState
└── Hook
└── queue
└── Update
这个 update 不只是“改 state”。
它还带着:
什么时候应该被处理
的信息。
22. Pending Work 会一路标记到 Root
更新发生在具体 Fiber。
但整个 React Root 需要知道:
我的某个 subtree 有 work
因此相关 lane 信息会沿 Fiber Tree 向上汇总:
Counter
↑
Parent
↑
Parent
↑
Root
最终 Root 维护类似:
pendingLanes
的信息。
React 才能决定:
下一次 render 应该处理哪些 work
23. Batch:多个 Update 不一定触发多次完整 Render
例如:
setA(...)
setB(...)
setC(...)
并不意味着必然:
render
render
render
React 会尽可能 batch:
enqueue A
enqueue B
enqueue C
↓
一次 render
这也是为什么理解:
update
schedule
render
三者不是同一个概念,很重要。
24. Update Render 时,useState(0) 为什么不重新变成 0?
下一次 render:
const [count, setCount] = useState(0)
React 已经知道这是 update,不是 mount。
它会按顺序找到旧 Fiber 对应的 Hook:
Hook
├── memoizedState = 0
└── queue
└── c => c + 1
然后处理 queue:
old state = 0
↓
action = c => c + 1
↓
new state = 1
所以这次函数执行中:
count = 1
useState(0) 里的 0 只是 mount 初始化时使用的初始值。
25. Reconciler 会把变化标成 Flags
新的 render 可能得到:
<button>1</button>
旧 UI 是:
<button>0</button>
Reconciler 在当前 child matching context 中发现:
type = button same
key = null same
对应的 tree position 可以继续匹配
所以 HostComponent Fiber identity 可以复用。
但文本:
"0" → "1"
发生变化。
于是对应 Fiber 会带上需要 commit 的标记。
可以粗略理解成:
flags = Update
这样 render phase 负责:
算出哪里要改
commit phase 负责:
真的去改
26. beginWork:Render Phase 如何向下展开 Tree
Fiber 不只是保存状态,也是一种可中断的工作单元。
Render phase 可以粗略理解成两步:
beginWork
completeWork
beginWork 的核心不是“看 effect flag”。
而是:
处理当前 Fiber,并决定它的 children 是什么。
对于 function component,大致:
beginWork(App)
↓
执行 App()
↓
拿到 React Elements
↓
reconcileChildren()
↓
创建 / 复用 child Fibers
↓
返回第一个 child
然后继续向下:
begin App
↓
begin A
↓
begin B
↓
...
27. completeWork:从叶子开始向上收尾
当某个 Fiber 没有更多 child 需要处理,就开始 complete。
例如:
App
└── A
├── B
└── C
遍历大致:
begin App
begin A
begin B
complete B
begin C
complete C
complete A
complete App
也就是:
beginWork ↓ 向下
completeWork ↑ 向上
completeWork 会做当前 Fiber 的收尾工作。
对于 Host Fiber,在 mount 时会准备对应 Host Instance;在 update 时则准备 Host Update。
在 Web 上,Host Instance 就是 DOM node。
28. flags 和 subtreeFlags
一个非常重要的 completeWork 工作是:
把 subtree 中的 commit work 信息往父节点冒泡。
例如:
App
└── A
└── B
flags = Update
B complete 后,父节点会汇总:
A.subtreeFlags
再继续向上:
App.subtreeFlags
于是 Root 最终知道:
这个 subtree 里有 commit work
Commit phase 就不需要把所有节点都当成有副作用处理。
可以根据:
flags
subtreeFlags
快速找到需要 commit 的区域。
29. Fiber 为什么有 child / sibling / return?
React 的 render traversal 不能依赖普通 JS 递归调用栈。
因为它需要:
pause
resume
interrupt
restart
abandon
所以 Fiber 自己必须携带树导航信息。
核心三个指针:
child
sibling
return
含义可以先理解成:
child → 第一个子节点
sibling → 下一个兄弟节点
return → 父节点
React 因此可以自己实现一套迭代式 DFS。
它可以在任意 Fiber 停下来,下次再从那个工作单元继续。
这正是 Fiber Architecture 的核心意义之一:
把原本不可中断的一整段递归渲染工作,拆成可调度的 work units。
30. 把整个 React Runtime 再串一次
现在可以把最核心主线压成:
index.html
↓
mount container
↓
createRoot
↓
root.render(<App />)
↓
React Element
↓
Fiber
↓
Component / Fiber Tree
↓
Context / Scope / Boundary
↓
Update Queue
↓
Lane
↓
Scheduler
↓
Current Tree
↕ alternate
WIP Tree
↓
beginWork
↓
reconciliation
↓
completeWork
↓
flags / subtreeFlags
↓
finishedWork
↓
Commit
↓
DOM Mutation
↓
Browser Rendering Pipeline
↓
Pixels
31. 最终 Mental Model
如果只保留几个最重要的抽象,可以记住下面这些。
1. React Element 是 Description
JSX
↓
React Element
它描述:
UI 应该是什么。
2. Fiber 是 Runtime Unit
Element
↓
Fiber
Fiber 保存:
identity
props
state
hooks
updates
lanes
flags
tree links
alternate
它既是运行时记录,也是工作单元。
3. State 属于 Fiber Identity
不是:
state 属于某个函数定义
而是:
state 属于这棵树里这个 component identity 对应的 Fiber
是否能保留 state,取决于 reconciliation 能不能把新的 Element 继续匹配到原来的 component identity。
可以压成:
tree position / sibling matching context
+
type
+
key
↓
old Fiber identity 是否能继续复用
↓
state 是否保留
不要把它机械记成一个固定的 type + key + position tuple;尤其在 keyed list 中,稳定 key 可以让 sibling reorder 后的 identity 继续匹配。
4. Component Tree 是 React 的空间模型
很多机制都可以理解成:
在树上建立 scope / boundary
↓
影响 subtree
例如:
Context
Suspense
Error Boundary
5. Render 是计算,Commit 是现实
Render
= 算下一棵 UI
Commit
= 把结果应用到 Host Environment
因此:
Render 可以被丢弃
Commit 不应该提交半成品
6. Parent Render 默认向下传播
Parent render
↓
children 默认继续 render / reconcile
但要区分:
Update Source
≠ Render Input
≠ Work Propagation
setState / Context / external notification
= work 从哪里进入 React
props / state / context
= render 读取什么
parent render
= work 怎么沿 tree 向下走
优化本质之一:
在某个 Fiber 上 bailout
↓
截断不必要工作
7. Update 不等于 DOM Change
update scheduled
≠ component 一定执行
≠ DOM 一定 mutation
≠ browser 一定 paint
这是性能分析时必须保持的层级感。
8. Lane 决定 React Work 的优先级关系
Update
↓
Lane
↓
Root pending work
↓
Scheduler
↓
Render
Concurrent React 的本质不是“真正并行”。
而是:
React 可以控制 work 的先后、暂停、恢复和丢弃。
9. React Runtime 和 External World 之间需要桥
React Runtime
Fiber / state / update / lane
↕
bridge
↕
External World
DOM / browser API / store / server data
几个方向要分清:
React → Host Environment
= Commit
React → External System
= Effect / library integration
External Store → React
= subscription / useSyncExternalStore / library adapter
因此 server state、query cache、Redux-like store 等,不应该因为“组件里能读到”就被理解成 Fiber 自己的 state。
32. 最后:React 可以被压缩成一棵树 + 两个世界
到最后,可以把整篇压成:
React Runtime
Component / Fiber Tree
identity / state / hooks / context
update / lane / reconciliation / bailout
│
┌──────────────┼──────────────┐
│ │ │
Commit Effect Subscription
│ │ │
↓ ↓ ↑
External World
DOM / Browser / Store / Server
第一条主线是 Tree:
React Element
↓
Fiber identity
↓
Update / Lane
↓
WIP Render
↓
Reconciliation / Bailout
↓
Commit
它解释:
state 为什么能保留
parent render 为什么会向下传播
memo 为什么能 bailout
Context 为什么既有 scope 又有 invalidation
Concurrent Render 为什么可以被暂停和丢弃
第二条主线是 Description → Reality:
Description
React Element / WIP Fiber calculation
↓
Commit
↓
Reality
DOM / Browser / Pixels
第三个必须同时记住的边界是 React Runtime ↔ External World:
外部 store / browser / server data
不天然属于 Fiber state
它们需要通过:
Effect
subscription
framework / data layer
等机制和 React Runtime 对接
于是 React 不再是一堆 API。
它更像一个围绕 Fiber Tree 运转、并和外部世界建立明确边界的 UI Runtime:
Update Source
↓
Schedule
↓
Render / Reconcile
↓
Bailout or Finish Work
↓
Commit
↓
Host Reality
真正理解 React,也不是记住更多 Hook。
而是看到一段代码时,能够问:
这个 Element 对应哪个 Fiber identity?
这个 state 挂在哪里?
这个 update 从哪里进入 React?
它进入哪个 queue?
它属于什么 lane?
这次 render 的 inputs 是什么?
work 会沿 tree 传播到哪里?
哪里可以 bailout?
是否存在 Context / external-store invalidation?
这次计算发生在 current 还是 WIP?
beginWork 在展开什么?
completeWork 在收尾什么?
最终哪些 flags 会进入 commit?
哪些数据其实存在 React 外部?
Commit 后浏览器还要付出什么成本?
当这些问题都能沿同一条 runtime 主线回答时,React 的很多“高级知识”其实已经不再高级。
它们只是:
同一棵 Fiber Tree 在不同阶段,以及 React Runtime 与外部世界边界上呈现出来的不同侧面。