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)
}

useStateuseEffectuseContextuseMemo 等普通 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. flagssubtreeFlags

一个非常重要的 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 与外部世界边界上呈现出来的不同侧面。