第 27 章:useTransition / useDeferredValue——低优先级更新实战
一个 5 万条数据的列表,输入框筛选。如果每次敲键都立刻重算整个列表,输入框会卡成幻灯片。useTransition 和 useDeferredValue 就是为这种场景生的:把"算列表"降级成低优先级,让"输入框"先响应。
先记住这一句:两个 API 是同一个思路的两种包装:
useTransition包"动作"(startTransition 包住 setState),useDeferredValue包"值"(派生值延迟更新)。包里的更新都走低优先的 Transition lane,高优的输入更新先落地,第 23 章的并发渲染负责"先停低优、跑高优"。
一图流:两个 API 怎么分工
useTransition:包住"动作"
mountTransition(ReactFiberHooks.js:3398)的实现很直白,它返回的 start 就是 startTransition:
const start = startTransition.bind(null, currentlyRenderingFiber, stateHook.queue, true, false);
return [false, start];
你在 start(() => setFilter(text)) 里包的 setState,会被标记成 Transition lane(第 22 章的 TransitionLanes,ReactFiberLane.js:61)。于是这次更新变成低优先:输入框的 onChange 直更新(InputContinuous lane)先渲染,列表的 transition 更新被"推迟、可中断",慢慢算。第一项返回值 isPending 告诉你 transition 还在进行,可以借此给列表挂个 loading 态。
useDeferredValue:包住"值"
mountDeferredValueImpl(3001)的思路是先返回旧值:
if (initialValue !== undefined && !isRenderingDeferredWork()) {
hook.memoizedState = initialValue; // 这一帧先用旧值/初始值渲染
const deferredLane = requestDeferredLane();
currentlyRenderingFiber.lanes = mergeLanes(..., deferredLane); // 排一个 deferred 渲染
return initialValue;
} else {
hook.memoizedState = value; // deferred 渲染时用新值
return value;
}
典型的写法 const deferredQuery = useDeferredValue(query):输入框绑 query(即时),列表绑 deferredQuery(滞后一拍的旧值)。你敲一个字,query 变、输入框立刻重渲染;deferredQuery 保持旧值,列表先用旧数据渲染一帧,然后 deferred lane 里重算列表。体验上"输入流畅、列表稍慢",而不是一起卡。
两者的关系
它们不是二选一,是同一机制的两层皮:
| useTransition | useDeferredValue | |
|---|---|---|
| 包什么 | 动作(setState) | 值(派生数据) |
| 有 isPending | ✅ | ❌ |
| 适用 | 你知道"哪个动作该降级" | 你知道"哪个值可以滞后" |
共同点都在并发渲染(第 23 章)的底座上:低优更新可中断、高优可插队、干不完下帧继续。没有并发渲染,这俩 API 无从谈起;有并发渲染,降级更新才能"优雅地慢慢算"。
动手实验
- 看 lane 变化:
start(() => setFilter(text))里包一次更新,在requestUpdateLane打断点,对比包在 transition 里和没包时拿到的 lane 不同(前者 Transition,后者 Default/InputContinuous)。 - 看 isPending:一个慢列表 +
useTransition,在startTransition打断点,观察isPending在 transition 进行期间是 true、结束是 false。 - 看 deferred 滞后:
useDeferredValue配一个会打印值的组件,快速输入几个字,观察列表组件渲染的次数少于输入次数(旧值被复用)。
小结
- 两个 API = 低优先 lane 的两种包装:transition 包动作,deferred 包值。
startTransition(3398)把包里的更新标记成 TransitionLanes(61)。useDeferredValue(3001)先返旧值,deferred lane 里再算新值。- 底座是并发渲染(第 23 章):低优可中断、高优插队。
- 下一步:隐藏而不卸载,第 28 章 Activity / Offscreen。