在前三篇中,我们构建了 Fiber 架构(骨架)、梳理了渲染流程(经络)、解析了 Hooks 原理(记忆)。现在,我们要解决最后一个、也是最直接影响用户体验的问题:

如何让应用在繁重的计算下依然保持丝滑的响应?

这就是 React 18 并发模式要回答的核心命题。


1. 经典难题:输入框卡顿之谜

想象一个常见的场景:搜索框

用户在输入框打字,下方需要根据输入内容渲染一个包含几千条数据的列表。

这里有两个性质完全不同的任务:

  • 输入回显(High Priority):用户敲键盘,屏幕需要立即显示字符,延迟超过 100ms 就会产生明显的”卡顿感”
  • 列表渲染(Low Priority):下方的搜索结果需要大量 DOM 操作,可能要跑几百毫秒

在 React 18 之前(或不加优化):

React 会把这两个更新当成一回事,放在同一个渲染任务里处理。一旦开始渲染列表,主线程就被占满。用户敲键盘,但屏幕上字出不来——因为浏览器正忙着画那几千条列表。


2. 并发模式的核心思想:任务分优先级

React 18 引入的并发模式(Concurrent Mode) 核心思想非常直觉:不是所有更新都一样紧急。

React 把更新分成了两大类:

  1. 紧急更新(Urgent updates):打字、点击、拖拽——这些必须在毫秒级内响应
  2. 过渡更新(Transition updates):渲染列表、切换页面——这些稍慢一点,用户完全可以接受

打个比方:

React 15 就像一条单车道高速公路——所有车都排着队。前面的大货车(列表渲染)开得慢,后面的跑车(用户打字)被堵死了。

React 18 开辟了 VIP 车道——跑车可以随时超车,甚至可以让大货车先靠边停(中断低优先级渲染),等跑车过去了,大货车再继续。


3. Lane 模型:React 的优先级系统

在 React 内部,优先级不是简单的”高 / 中 / 低”,而是用一套精心设计的位运算系统来表达的,叫做 Lane 模型

用二进制位表达优先级

每条”车道(Lane)”对应一个二进制位。位置越低(越靠右),优先级越高:

1
2
3
4
5
6
7
8
9
10
// React 源码中的 Lane 定义(简化)
const NoLane = 0b0000000000000000000000000000000;
const SyncLane = 0b0000000000000000000000000000010; // 同步,最高
const InputContinuousLane = 0b0000000000000000000000000001000; // 连续输入(拖拽)
const DefaultLane = 0b0000000000000000000000000100000; // 默认
const TransitionLane1 = 0b0000000000000000000001000000000; // 过渡 1
const TransitionLane2 = 0b0000000000000000000010000000000; // 过渡 2
// ... 一共 31 条 Lane
const IdleLane = 0b0100000000000000000000000000000; // 空闲
const OffscreenLane = 0b1000000000000000000000000000000; // 离屏

为什么用位运算

位运算让优先级的合并、比较、过滤变得非常高效:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 合并多个 Lane(用 OR)
const mergedLanes = SyncLane | DefaultLane;

// 判断某个 Lane 是否被包含(用 AND)
const isSyncIncluded = (mergedLanes & SyncLane) !== 0; // true

// 从集合中移除某个 Lane(用 AND NOT)
const remaining = mergedLanes & ~SyncLane; // 只剩 DefaultLane

// 取最高优先级的 Lane(取最低位的 1)
function getHighestPriorityLane(lanes) {
return lanes & -lanes;
}

Lane 如何影响渲染

当一个 setState 被调用时,React 会给这个更新分配一条 Lane:

1
2
3
4
5
6
7
8
9
10
11
12
13
function requestUpdateLane(fiber) {
// 如果当前在 transition 中,分配 TransitionLane
if (isTransition) {
return claimNextTransitionLane();
}

// 根据事件类型判断
const eventLane = getCurrentEventPriority();
// 点击 → SyncLane
// 拖拽 → InputContinuousLane
// 默认 → DefaultLane
return eventLane;
}

然后在 Render 阶段,React 只处理当前批次选中的 Lane 对应的更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
function beginWork(current, workInProgress, renderLanes) {
// ...

// 该节点上如果有 update,但 update 的 Lane 不在 renderLanes 中
// React 会跳过这个 update,等后续批次再处理
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// 优先级不够,先跳过
return;
}

// 优先级匹配,处理这个更新
processUpdateQueue(workInProgress, renderLanes);
}

这就是”高优先级打断低优先级”的底层机制:

  1. 低优先级的 Transition 更新开始渲染(分配了 TransitionLane)
  2. 用户敲键盘,产生一个 SyncLane 的更新
  3. Scheduler 发现有更高优先级的任务,中断当前渲染
  4. 用 SyncLane 重新从根节点开始 Render,只处理高优先级的更新
  5. 高优先级渲染完毕、Commit 完毕后,再回来继续(或重启)低优先级的渲染

4. useTransition:手动标记”不着急”的更新

有了 Lane 模型的底层支持,React 提供了 useTransition Hook,让开发者手动告诉 React:”这个更新没那么急。”

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useState, useTransition } from 'react';

function SearchList() {
const [input, setInput] = useState('');
const [list, setList] = useState([]);
const [isPending, startTransition] = useTransition();

const handleChange = (e) => {
// 紧急任务:直接更新,保证输入框瞬间响应
setInput(e.target.value);

// 过渡任务:用 startTransition 包裹
startTransition(() => {
const results = heavyFilterFunction(e.target.value);
setList(results);
});
};

return (
<div>
<input value={input} onChange={handleChange} />
{isPending ? <p>加载中...</p> : <List data={list} />}
</div>
);
}

底层发生了什么

当你调用 startTransition(callback) 时,React 内部的处理流程是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function startTransition(callback) {
// 1. 先把 isPending 置为 true(这个 setState 是高优先级的!)
setPending(true);

// 2. 设置一个标记,表示后续的 setState 应该使用 TransitionLane
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = {};

try {
// 3. 执行回调——里面的 setState 会被分配 TransitionLane
callback();

// 4. 再把 isPending 置为 false(也是 TransitionLane)
setPending(false);
} finally {
// 5. 恢复标记
ReactCurrentBatchConfig.transition = prevTransition;
}
}

分解一下优先级分配:

操作分配的 Lane优先级
setPending(true)SyncLane(正常 setState)
setList(results)TransitionLane(在 transition 上下文中)
setPending(false)TransitionLane

这意味着:

  1. setPending(true) 立即被处理,UI 上马上显示 Loading
  2. setList(results) 被标记为低优先级,React 在”空闲时间”慢慢渲染
  3. 如果用户继续打字,React 会中断列表渲染,优先处理新的输入
  4. 等列表渲染完成,setPending(false) 也会一起被提交,Loading 消失

5. useDeferredValue:另一种标记”不着急”的方式

除了 useTransition,React 18 还提供了 useDeferredValue,它从另一个角度来解决同样的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
function SearchList({ query }) {
// deferredQuery 会"延迟"跟上 query 的更新
const deferredQuery = useDeferredValue(query);

// 当 query 和 deferredQuery 不一致时,说明正在"追赶"
const isStale = query !== deferredQuery;

return (
<div style={{ opacity: isStale ? 0.6 : 1 }}>
<List query={deferredQuery} />
</div>
);
}

useTransition vs useDeferredValue

维度useTransitionuseDeferredValue
控制对象控制 setState 的优先级控制的更新时机
使用场景你能直接控制 setState 的地方值来自 props 或外部,你控制不了更新方式
返回值[isPending, startTransition]延迟后的值
底层实现直接修改 Lane 分配内部触发一个低优先级的 setState

简单来说:

  • 如果你能控制 setState,用 useTransition
  • 如果值是从 props 传进来的,你控制不了上层怎么更新,用 useDeferredValue

6. 终极对比:Debounce vs Throttle vs useTransition

这是”面试杀手级”问题:为什么不用防抖(Debounce)或节流(Throttle)?

1
2
3
4
5
6
7
8
9
10
11
// 方案 A:防抖
const handleChange = (e) => {
setInput(e.target.value);
debounce(() => setList(filter(e.target.value)), 300);
};

// 方案 B:useTransition
const handleChange = (e) => {
setInput(e.target.value);
startTransition(() => setList(filter(e.target.value)));
};

看起来效果差不多?本质区别很大:

特性Debounce(防抖)Throttle(节流)useTransition(并发)
策略消极等待定频执行积极工作
原理强制让 CPU 休息(等 300ms)每隔固定时间执行一次利用按键间隙尝试渲染
CPU 利用率低(等待期空转)中等高(榨干每一毫秒)
用户体验停止输入后还要等一会固定延迟即时响应,不卡顿
是否中断旧渲染否(只是延迟触发)(旧渲染被丢弃)
响应速度最慢(必须等超时)中等最快(有空就渲染)
等待时间固定(300ms)固定动态(取决于 CPU 空闲)

核心差异用一句话概括:

Debounce 是让任务晚点开始,useTransition 是让任务可以被打断。

Debounce 在等待的 300ms 里 CPU 什么都没干——白白浪费了。而 useTransition 在用户两次按键之间的几十毫秒间隙里,可能已经渲染了一部分列表。如果用户不再打字,列表会比 debounce 方案更快出现;如果用户继续打字,旧的渲染会被中断,不会卡顿。


7. 并发模式的本质:可中断的渲染

回顾整个系列,并发模式并不是一个独立的”新功能”,而是前三篇所有基础设施的最终组合

这四层叠在一起,才实现了”高优先级可以打断低优先级”这个看似简单的用户体验优化。


8. 全系列总结

至此,我们已经完成了 React 核心原理的四大拼图:

篇章主题解决的问题核心概念
第一篇Fiber 架构渲染不可中断导致卡顿Fiber 链表、时间切片、Scheduler
第二篇渲染流程更新过程中 UI 可能不一致双缓存、Render/Commit 分离
第三篇Hooks 原理函数组件如何保存状态闭包快照、链表存储、更新队列
第四篇并发模式繁重计算导致输入卡顿Lane 优先级、useTransition

这四篇的关系是层层递进的:

拥有了这套思维模型,再去看 React 的源码或面对面试难题,你将不再是死记硬背,而是能够从设计哲学的角度去理解每一行代码为什么要那样写。


相关链接

Happy Coding!