Skip to main content

第 27 章:useTransition / useDeferredValue——低优先级更新实战

一个 5 万条数据的列表,输入框筛选。如果每次敲键都立刻重算整个列表,输入框会卡成幻灯片。useTransitionuseDeferredValue 就是为这种场景生的:把"算列表"降级成低优先级,让"输入框"先响应

先记住这一句:两个 API 是同一个思路的两种包装:useTransition 包"动作"(startTransition 包住 setState),useDeferredValue 包"值"(派生值延迟更新)。包里的更新都走低优先的 Transition lane,高优的输入更新先落地,第 23 章的并发渲染负责"先停低优、跑高优"。

一图流:两个 API 怎么分工

useTransition:包住"动作"

mountTransitionReactFiberHooks.js:3398)的实现很直白,它返回的 start 就是 startTransition

const start = startTransition.bind(null, currentlyRenderingFiber, stateHook.queue, true, false);
return [false, start];

你在 start(() => setFilter(text)) 里包的 setState,会被标记成 Transition lane(第 22 章的 TransitionLanesReactFiberLane.js:61)。于是这次更新变成低优先:输入框的 onChange 直更新(InputContinuous lane)先渲染,列表的 transition 更新被"推迟、可中断",慢慢算。第一项返回值 isPending 告诉你 transition 还在进行,可以借此给列表挂个 loading 态。

useDeferredValue:包住"值"

mountDeferredValueImpl3001)的思路是先返回旧值

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 里重算列表。体验上"输入流畅、列表稍慢",而不是一起卡

两者的关系

它们不是二选一,是同一机制的两层皮:

useTransitionuseDeferredValue
包什么动作(setState)值(派生数据)
有 isPending
适用你知道"哪个动作该降级"你知道"哪个值可以滞后"

共同点都在并发渲染(第 23 章)的底座上:低优更新可中断、高优可插队、干不完下帧继续。没有并发渲染,这俩 API 无从谈起;有并发渲染,降级更新才能"优雅地慢慢算"。

动手实验

  1. 看 lane 变化start(() => setFilter(text)) 里包一次更新,在 requestUpdateLane 打断点,对比包在 transition 里和没包时拿到的 lane 不同(前者 Transition,后者 Default/InputContinuous)。
  2. 看 isPending:一个慢列表 + useTransition,在 startTransition 打断点,观察 isPending 在 transition 进行期间是 true、结束是 false。
  3. 看 deferred 滞后useDeferredValue 配一个会打印值的组件,快速输入几个字,观察列表组件渲染的次数少于输入次数(旧值被复用)。

小结

  • 两个 API = 低优先 lane 的两种包装:transition 包动作,deferred 包值。
  • startTransition(3398)把包里的更新标记成 TransitionLanes(61)。
  • useDeferredValue(3001)先返旧值,deferred lane 里再算新值。
  • 底座是并发渲染(第 23 章):低优可中断、高优插队。
  • 下一步:隐藏而不卸载,第 28 章 Activity / Offscreen。