让动态世界拥有确定性(二):React 如何让 UI 更新变得可调度
setState、Fiber Tree、Lanes Priority、Scheduler 与 Concurrent Rendering 如何协作,让一次状态更新从同步阻塞变成可暂停、恢复、抢占的任务
上一篇我们看了 V8 如何在运行时给动态对象偷偷加“结构描述”,让属性访问从查找变成直接偏移。
但 V8 的优化有一个前提:代码的执行路径是可预测的。
如果代码本身写得混乱——频繁改变对象结构、传入各种类型的参数——V8 也无能为力。
那到了应用层呢?当用户在搜索框里输入一个字母,React 需要做的事情是:
用户输入 → setState → 重新计算组件树 → 更新 DOM
如果搜索结果计算很慢,输入框会卡住吗?
在 React 18 引入 Concurrent Rendering 之前,render 阶段是同步不可中断的。一旦开始计算更新,React 必须一路完成,算完之前用户什么都做不了。
React 19 的并发模型要解决的就是:
让一次状态更新,从立即执行的同步任务,变成可以暂停、恢复、抢占的任务。
一、问题:setState 是同步的
先看最朴素的 React:
function SearchBox() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
function handleChange(e) {
setQuery(e.target.value)
setResults(search(e.target.value)) // 假设很慢
}
return (
<>
<input value={query} onChange={handleChange} />
<ResultList results={results} />
</>
)
}
React 的默认行为:一次 setState 就是一次同步更新。
用户输入 "a"
→ setQuery("a")
→ setResults(search("a")) ← 假设要 200ms
→ 等待 200ms
→ DOM 更新
→ 用户才能输入下一个字母
两个问题:
- 输入框卡住了。 用户敲完一个字母后要等 200ms 才能继续输入。
- 结果可能过时。 用户快速输入 “abc”,但你算的是 “a” 的结果。
这不是 React 的 bug,而是设计选择:React 默认把所有 setState 视为同等优先级的同步任务。
二、Fiber:让更新可以被打断
要让同步任务变成可中断的任务,React 需要一个数据结构来表示“工作进度”,这样停下来的时候知道从哪里继续。
这就是 Fiber。
旧架构的问题
React 15 之前的 Reconciler(协调器)是递归的:
更新 Root
↓
遍历整棵组件树(递归,同步,不可中断)
↓
完成所有组件的 diff
↓
更新 DOM
递归遍历意味着:一旦开始,必须跑完。 不能中途停下来去做更紧急的事。
如果组件树很大,一次更新可能需要几十毫秒甚至上百毫秒,整个过程里浏览器无法处理用户输入。
Fiber 的核心思路
Fiber 仍然是树结构,但它通过 child、sibling、return 三个指针,把递归遍历转换成可暂停的迭代遍历:
React 15(递归,同步不可中断):
Root → Child → Child → Child → 回溯 → Child → ...
一旦开始,一口气跑完。
React 18+(迭代遍历,可暂停):
Root
↓
Child1
↓
Child2 → 工作做了一半 → 可以暂停
↓
Child3
每个 Fiber 节点仍然是树的一部分,但它是一个 JavaScript 对象,包含:
{
type: 'div',
key: null,
stateNode: DOM节点,
child: 子节点,
sibling: 兄弟节点,
return: 父节点,
pendingProps: 新props,
memoizedState: 当前state,
updateQueue: 待处理的更新,
}
关键字段是 child、sibling、return——它们让 React 可以用迭代代替递归来遍历这棵树。
React 可以在任何 Fiber 节点上停下来:
处理 Root → 处理 Child1 → 处理 Child2
↓
"有更紧急的任务"
↓
暂停,保存进度
↓
处理紧急任务
↓
回来,从 Child2 继续
Fiber 让 React 的工作变成了可调度的任务。
三、Lanes:给任务分优先级
Fiber 解决了“可以中断”的问题。但下一个问题来了:
什么时候该中断?中断去做什么?
答案是 Lanes(车道模型)。
Lanes 是 React 内部的优先级系统。每个更新被分配到一个 Lane,不同的 Lane 代表不同的优先级:
// React 内部的 Lane 定义(简化)
SyncLane = 0b0000000000000000000000000000001 // 同步,最高优先级
InputContinuousLane = 0b0000000000000000000000000000100 // 连续输入(拖拽)
DefaultLane = 0b0000000000000000000000000010000 // 普通更新
TransitionLane = 0b0000000000000000000001000000000 // startTransition
IdleLane = 0b0100000000000000000000000000000 // 空闲
为什么用位运算?因为 React 需要高效地做批量合并:
Lane A = 0b001
Lane B = 0b010
A | B = 0b011 // 合并后的任务集
位运算让 React 可以在一个整数里同时表示多个优先级的任务,O(1) 复杂度完成合并和检查。
调度规则
核心规则:高优先级可以抢占低优先级,低优先级不能抢占高优先级。
用户输入 "a"
→ setQuery("a") → SyncLane(最高优先级)
→ setResults(search("a")) → TransitionLane(低优先级)
React 开始处理 TransitionLane 的任务(计算搜索结果)
用户继续输入 "b"
→ setQuery("ab") → SyncLane(最高优先级)
React 立刻:
1. 暂停当前 TransitionLane 任务
2. 执行 SyncLane 任务(更新输入框)
3. 有空闲时再恢复 TransitionLane
这就是为什么 React 18+ 的输入框不会卡住——输入是 SyncLane,永远能抢占。
useTransition:手动标记低优先级
Lanes 是内部机制,开发者通过 useTransition 来使用它:
function SearchBox() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
function handleChange(e) {
setQuery(e.target.value)
startTransition(() => {
setResults(search(e.target.value)) // 标记为 TransitionLane
})
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending ? <Spinner /> : <ResultList results={results} />}
</>
)
}
setQuery(e.target.value) → SyncLane,立即执行
startTransition(() => setResults(..)) → TransitionLane,可以被打断
startTransition 做的事情很简单:把里面的 setState 标记为 Transition 优先级。
React 不需要你手动管理调度——它只需要你告诉它哪些更新可以被推迟。
四、Scheduler:React 的调度器
Fiber 是“可中断的工作单元”,Lanes 是“优先级标签”。
但还有一个问题:什么时候执行这些任务?
React 不直接控制浏览器主线程。它需要一个调度器来决定“什么时候工作,什么时候让步”。
这就是 Scheduler(调度器)。
时间切片(Time Slicing)
Scheduler 的核心机制是时间切片:
浏览器给 JavaScript 的帧预算:约 16ms(60fps)
React 的做法:
1. 开始执行一个时间切片(5ms)
2. 时间到了 → 停下来,把控制权还给浏览器
3. 浏览器处理用户输入、绘制帧
4. 浏览器空闲 → React 继续下一个时间切片
用 MessageChannel(不是 setTimeout)实现:
React 任务
↓
requestIdleCallback / MessageChannel
↓
浏览器空闲时执行
↓
5ms 到了
↓
暂停,yield 回主线程
↓
浏览器处理其他事件
↓
空闲时 React 继续
Scheduler 与 Lanes 的协作
Lanes 告诉 React:哪些任务优先级高
Scheduler 告诉 React:什么时候可以执行
完整流程:
1. 用户输入 → setQuery → SyncLane
2. React 收集更新,按 Lane 排序
3. Scheduler 调度:SyncLane 任务立即执行
4. React 处理 Fiber 树,每个时间切片 5ms
5. 时间到了 → yield → 浏览器处理输入 → 继续
6. startTransition 里的 setResults → TransitionLane
7. 空闲时 Scheduler 才调度 TransitionLane 任务
8. 用户继续输入 → SyncLane 抢占 → 暂停 TransitionLane
五、Concurrent Rendering:完整的并发流程
把所有机制串起来,一次状态更新的完整路径是:
用户点击 / 输入
↓
事件处理器触发 setState
↓
React 根据调用方式分配 Lane
- 普通 setState → SyncLane
- startTransition → TransitionLane
↓
更新被添加到对应 Fiber 节点的 updateQueue
↓
Scheduler 调度一个宏任务
↓
React 遍历 Fiber 树(链表遍历)
- 每个节点处理 props / state diff
- 每个时间切片约 5ms
- 如果有更高优先级的更新 → yield
↓
高优先级 Lane 插入 → 暂停当前工作 → 优先处理
↓
所有 Lane 处理完毕
↓
提交阶段(commit):一次性更新 DOM
↓
浏览器绘制
关键设计:Diff 阶段可以中断,Commit 阶段不可中断。
Diff(render 阶段):
- 遍历 Fiber 树,计算变化
- 可以暂停、恢复、丢弃
- 不产生任何 DOM 变化
Commit(commit 阶段):
- 一次性把所有变化应用到 DOM
- 不可中断(避免中间状态暴露给用户)
- 浏览器在 commit 之后绘制
六、React 19 的变化
React 19 在这个架构上做了几个调整:
async startTransition
React 18 的 startTransition 只支持同步函数。React 19 允许异步:
// React 18
startTransition(() => {
setResults(search(query)) // 同步
})
// React 19
startTransition(async () => {
const data = await fetchResults(query) // 异步
setResults(data)
})
useOptimistic:乐观更新
在异步操作等待期间,先展示一个“乐观”的中间状态:
const [optimisticItems, addOptimistic] = useOptimistic(items)
async function handleBuy() {
addOptimistic({ id: newItemId, bought: true })
await buyItem()
refreshItems()
}
addOptimistic 不会触发真正的状态更新,而是临时展示一个预测值。异步操作完成后自动恢复真实状态。
useActionState / useFormStatus:标准化表单交互
async function submitForm(prevState, formData) {
const email = formData.get('email')
const { error } = await submitEmail(email)
if (error) return { error }
return { success: true }
}
function ContactForm() {
const [state, formAction, isPending] = useActionState(submitForm, null)
return (
<form action={formAction}>
<input name="email" type="email" />
{state?.error && <p className="error">{state.error}</p>}
<button disabled={isPending}>
{isPending ? '提交中...' : '提交'}
</button>
</form>
)
}
这些 API 不是“新功能”,而是并发架构的标准化封装。底层还是 Fiber + Lanes + Scheduler。
七、与 V8 的对应
回到系列主题“让动态世界拥有确定性”:
V8:
对象结构动态 → Hidden Class 给出结构描述
属性访问动态 → IC 缓存访问路径
代码行为动态 → TurboFan 生成优化机器码
React:
状态更新动态 → Fiber 让更新可中断
任务优先级动态 → Lanes 给出优先级标签
执行时机动态 → Scheduler 决定何时执行
V8 在运行时观察代码行为,React 在运行时观察用户行为。
两者都在做同一件事:
在动态系统中,找到一种确定性模式,然后利用它做针对性优化。
总结
React 的并发架构不是一次状态更新的“加速”,而是一次认知转变:
从“setState = 立刻执行”到“setState = 提交一个待调度的任务”。
setState
↓
Fiber:让任务可以被中断
Lanes:让任务有优先级
Scheduler:让任务在合适的时间执行
↓
Concurrent Rendering
核心问题的答案:
一次状态更新如何从同步阻塞变成可调度的?
通过把“立即执行”拆成“描述任务 + 分配优先级 + 调度执行”三步。
下一篇
React 让你配合它的规则来获得性能——按它的模式写,它来帮你调度更新。
但有人会想:能不能不让开发者配合,直接在编译阶段把该优化的都优化了?
Vue Vapor 做的就是这个尝试。
《让动态世界拥有确定性(三):Vue Vapor 如何用编译换运行时》
V8 优化代码执行
React 优化任务调度
Vue Vapor 优化代码生成