创见博客
useTransition与useDeferredValue的底层原理
七崽爱吃小饼干2026/01/09阅读 0专栏 React

useDeferredValue & useTransition 底层原理深度解析

前置结论:useDeferredValue 和 useTransition 共享 100% 的底层调度内核,没有第二个原理,所有差异都只是 React 做的「语法糖封装」。

一、先铺垫:理解原理的 3 个核心前置概念

这两个 Hook 的原理完全基于 React18 新增的「并发渲染」能力,所有底层逻辑都围绕这 3 个概念展开,缺一不可,理解了这3个概念,原理就懂了80%:

概念1:React18 的「更新优先级调度机制」(核心基石)

React 从 18 版本开始,给每一次组件更新(render) 都打上了「优先级标记(lane)」,核心规则:

  1. React 内部维护了一套优先级队列,所有需要触发的组件更新,都会带着「优先级」进入队列排队;
  2. 高优先级更新插队执行,低优先级更新排队等待;
  3. 浏览器的主线程执行规则:高优先级任务(输入、点击、滚动、动画)> React高优先级更新 > React低优先级更新 > 浏览器空闲任务;
  4. 关键特性:可中断(Interruptible)+ 可恢复(Resumable) —— 如果低优先级更新正在执行,此时来了新的高优先级更新,React 会立即暂停低优先级更新,先执行高优先级的,等高优执行完、浏览器彻底空闲后,再恢复执行刚才暂停的低优更新。

注意:React17及之前是「同步渲染」,没有优先级概念,一个更新开始就必须执行到底,无法中断,这也是为什么旧版本会出现「输入卡顿」的核心原因。

概念2:两种更新的优先级定义(直接对应这两个Hook)

React 内部对更新的优先级做了明确划分,这两个 Hook 本质就是操作这两种优先级:

  1. 紧急更新(Urgent Update)/ 高优先级更新

    • 对应场景:输入框输入、按钮点击、表单提交、鼠标悬停、动画帧刷新等用户交互相关的更新;
    • 特征:必须立即响应,0延迟,用户能感知到卡顿的更新,优先级最高,永远插队执行;
    • 原生触发:普通的 setState、useState 的更新,默认都是「紧急更新」。
  2. 非紧急更新(Non-Urgent Update)/ 低优先级更新

    • 对应场景:大数据列表渲染、搜索结果筛选、长列表排序、复杂计算后的视图更新等纯视图展示相关的更新;
    • 特征:用户对延迟几帧感知不到,晚一点更新完全不影响体验,优先级最低,排队执行;
    • 核心触发:useDeferredValue 和 useTransition 本质就是把「原本的高优更新」降级为「低优更新」。

概念3:「过期时间」与「自动合并多次更新」(防抖的底层原理)

这是「天然防抖效果」的底层原因,非常关键: React 给所有低优先级更新都设置了「过期时间」,当同一个值/同一个状态高频触发更新(比如输入框每秒输入10次)时:

  • React 不会为每一次变化都发起独立的低优更新,而是会合并多次更新为一次,只保留「最新的最终值」发起一次低优更新;
  • 这个过程完全是 React 内部自动完成的,不需要开发者手写防抖/节流,这就是为什么用这两个 Hook 处理输入联动时,天然有防抖效果的原因。

二、核心原理:useDeferredValue & useTransition 底层完全一致的内核

核心结论

useDeferredValue 和 useTransition 的底层实现,共用同一个「低优先级调度内核」,它们的底层逻辑、优先级规则、中断恢复机制、合并更新策略,完全一模一样,没有任何区别。

它们的底层核心逻辑只有一句话:

把目标更新的优先级从「紧急优先级」降级为「非紧急优先级」,让这个更新进入「低优先级队列」,等待所有高优先级更新执行完毕、浏览器主线程空闲后,再执行这个低优先级更新,并且在执行中可被新的高优更新中断。

所有的细节都基于这个核心逻辑展开:

  1. 低优更新排队时,页面不会卡顿,因为高优更新(比如输入)优先执行;
  2. 低优更新执行中,如果用户又输入了新内容(新的高优更新),React 会立即暂停低优更新,先处理输入,输入完成后再继续执行低优更新;
  3. 高频触发的低优更新会被自动合并,只执行最后一次,实现「天然防抖」。

三、为什么底层完全一样,却要设计两个不同的 Hook?(核心灵魂问题)

这是你一定会思考的问题,也是理解这两个 Hook 设计初衷的关键,答案非常清晰:

因为「底层原理相同」,但「上层的使用场景、操作对象、封装形式不同」,React 为了让开发者「更优雅、更低成本」的处理不同业务场景,才封装了两种不同的 API 形态,本质是「同核不同皮」的语法糖。

简单说:一个内核,两种用法,适配不同场景。 React 团队的设计思路:

  • 为「操作值」的场景,封装了 useDeferredValue;
  • 为「操作状态更新」的场景,封装了 useTransition。

四、两者的「底层内核相同,上层封装不同」

这是对上一轮「区别」的原理级补充,也是你最关心的:为什么底层一样,用法和表现却有区别? 所有的区别,都不是「底层原理不同」,而是「React 对底层内核的封装方式不同」,所有差异都来自「上层封装」,我们分两个 Hook 逐一拆解,看完你会彻底打通任督二脉:

1. useTransition 的封装逻辑:「主动标记更新」的语法糖

useTransition 是对底层「低优先级调度内核」的**「主动式封装」**,它的封装设计思路:

暴露一个 startTransition 函数,开发者主动把「需要降级的 setState 操作」包裹进去,React 内部会拦截这个函数内的所有 setState,将这些 setState 触发的更新,全部标记为「低优先级」,然后送入低优队列等待执行。

补充细节(源码级)

  • useTransition 返回的 isPending 是 React 给开发者的「贴心赠品」:React 内部能感知到「低优先级更新是否在排队/执行中」,所以直接把这个状态暴露出来,让开发者可以轻松实现「加载中」的交互反馈;
  • startTransition 只能包裹「状态更新操作(setState/useState/useReducer的dispatch)」,不能包裹普通逻辑,因为它的封装目标就是「修改更新的优先级」。

核心特征(来自封装):主动、显式、操作「更新」

  • 主动:需要开发者手动调用 startTransition 包裹更新逻辑;
  • 显式:能明确看到「哪些更新被降级了」;
  • 操作对象:状态更新行为(setState)。

2. useDeferredValue 的封装逻辑:「被动延迟值」的语法糖

useDeferredValue 是对底层「低优先级调度内核」的**「被动式封装」**,它的封装设计思路:

接收一个「原始值(state/props/变量)」,React 内部基于这个原始值,创建一个「延迟镜像值」,当原始值发生变化触发高优先级更新时,这个「延迟镜像值」不会立即同步变化,而是等待所有高优更新执行完毕后,再同步为最新的原始值,进而触发依赖这个延迟值的组件渲染。

补充细节(源码级)

  • useDeferredValue 本质是创建了一个「依赖原始值的、低优先级的派生状态」:原始值是高优的,延迟值是低优的,原始值变了,延迟值会在低优队列中同步;
  • 它没有暴露「pending状态」,因为它的封装目标是「值」,而不是「更新」,React 内部也能感知到延迟值是否在同步,但没有封装这个状态,所以需要开发者自己实现加载态。

核心特征(来自封装):被动、隐式、操作「值」

  • 被动:不需要手动触发,原始值变化后,延迟值会自动同步,无需开发者干预;
  • 隐式:看不到「降级」的过程,只看到「值的延迟变化」;
  • 操作对象:数据值(state/props/变量/对象/数组)。

五、补充 3 个源码级的底层细节(加分项,面试高频)

这部分是原理的延伸,都是 React 源码里的真实逻辑,也是面试中常问的细节,帮你锦上添花:

细节1:两者都不是 setTimeout,也不是防抖节流

很多人误以为这两个 Hook 是「用 setTimeout 实现的延迟」或「内置了防抖节流」,这是错误的:

  • setTimeout 是「固定时间延迟」,不管浏览器是否空闲,到时间就执行,优先级不可控;
  • 防抖节流是「固定时间合并」,也是人为控制时间;
  • 而这两个 Hook 的延迟是「基于浏览器空闲的动态延迟」,由 React 的调度器(Scheduler)根据主线程任务队列动态判断,什么时候空闲什么时候执行,优先级是原生的浏览器任务优先级,远比 setTimeout 智能。

细节2:两者的「低优先级」是 React 内部优先级,不是浏览器优先级

React 的优先级和浏览器的优先级是「映射关系」,不是「等同关系」:

  • React 的「紧急更新」映射到浏览器的「高优先级任务」(Input、Animation、Paint);
  • React 的「非紧急更新」映射到浏览器的「空闲任务」(Idle Callback);
  • 这个映射由 React 的 Scheduler 调度器完成,是 React 自己实现的一套优先级体系,和浏览器原生的 requestIdleCallback 同源,但做了兼容性和性能优化。

细节3:两者都支持「嵌套更新」和「多次降级」

  • 可以在 startTransition 里嵌套调用 startTransition,也可以在 useDeferredValue 里嵌套使用 useDeferredValue;
  • 但多次降级不会让优先级更低,因为 React 只有「紧急」和「非紧急」两种核心优先级(底层有更细的划分,但对外暴露的只有这两种),多次降级还是「非紧急优先级」,没有区别。

六、原理+用法 终极总结

  1. 底层同源:useDeferredValue 和 useTransition 共享同一个低优先级调度内核,底层原理、优先级规则、中断恢复、合并更新完全一致,核心都是「降级更新优先级」;
  2. 上层异构:差异全部来自 React 的上层封装,一个是「主动标记更新」的封装,一个是「被动延迟值」的封装,操作对象不同,适配场景不同;
  3. 选型原则:要「延迟一个值的生效」,用 useDeferredValue(低侵入、简单场景);要「延迟一个/多个 setState 操作」或「需要加载态」,用 useTransition(高灵活、复杂场景)。
评论
0/100