详解React的Transition工作原理原理
创始人
2025-05-28 04:17:01

Transition 使用姿势

Transition 是 react18 引入的新概念,用来区分紧急和非紧急的更新。

  • 紧急的更新,指的是一些直接的用户交互,如输入、点击等;
  • 非紧急的更新,指的是 UI 界面从一个样子过渡到另一个样子;

react 官方的 demo 如下:

import {startTransition} from 'react';// Urgent: Show what was typed
setInputValue(input);// Mark any state updates inside as transitions
startTransition(() => {// Transition: Show the resultssetSearchQuery(input);
});

有 2 个 API:

  • useTransition:hook,用在 function 组件或其他 hooks 中,能返回 isPending;
  • startTransition:用在不能使用 hooks 的场景,如 class 组件中,相比 useTransition 不能获取 isPending 状态;

2 个 API 还有一个差别:当进行连续快速输入时,使用  startTransition  是无法触发类似 throttle 的效果的。

Transition VS throttle、debounce

存在的问题:

  • 到达指定时间后,更新开始处理,渲染引擎会被长时间阻塞,页面交互会出现卡顿;
  • throttle 的最佳时间不易掌握,是由开发者设置的时间。而这个预设的时间,在不同性能的设备上不一定能带来最佳的体验;

存在的问题:

  • 会出现用户输入长时间得不到响应的情况,如上例中虽然输入框中内容一直在变但下面区域内一直不变;
  • 更新操作正式开始以后,渲染引擎仍然会被长时间阻塞,依旧会存在页面卡死的情况;

用 transition 机制的效果:

  • 用户可以及时看到输入内容,交互也较流畅;
  • 用户连续输入时,不会一直得不到响应(最迟 5s 必会开始更新渲染列表);
  • 开始更新渲染后,协调过程是可中断的,不会长时间阻塞渲染引擎(进入浏览器渲染阶段依然会卡住);

transition 相比前两种方案的优势:

  • 更新协调过程是可中断的,渲染引擎不会长时间被阻塞,用户可以及时得到响应;
  • 不需要开发人员去做额外的考虑,整个优化过程交给 react 和浏览器即可;

transition 实现原理

isPending 实现原理

我们看到页面首先进入了 pending 状态,然后才显示为 transition 更新后的结果。这里发生了 2 次 react 更新。但我们只写了一个 setState。

function App() {const [value, setValue] = useState("");const [isPending, startTransition] = useTransition();const handleChange = (e) => {const newVal = e.target.value;startTransition(() => setValue(newVal));};return (
handleChange} />
isPending ? 'loading' : ''}>{Array(50000).fill("a").map((item, index) => {return
index}>{value}
;})}
); }

我们看一下 useTransition 源码:

useTransition(): [boolean, (() => void) => void] {currentHookNameInDev = 'useTransition';mountHookTypesDev();return mountTransition();
},function mountTransition(): [boolean, (callback: () => void, options?: StartTransitionOptions) => void] {const [isPending, setPending] = mountState(false);// The `start` method never changes.const start = startTransition.bind(null, setPending);const hook = mountWorkInProgressHook();hook.memoizedState = start;return [isPending, start];
}function startTransition(setPending, callback, options) {const previousPriority = getCurrentUpdatePriority();setCurrentUpdatePriority(higherEventPriority(previousPriority, ContinuousEventPriority),);setPending(true);const prevTransition = ReactCurrentBatchConfig.transition;ReactCurrentBatchConfig.transition = {};...try {setPending(false);callback();} finally {setCurrentUpdatePriority(previousPriority);ReactCurrentBatchConfig.transition = prevTransition;...}
}

当调用 startTransition 时,会先通过 setPending 将 isPending 改为 true,然后再通过 setPending 将 isPending 改为 false,并在 callback 中触发我们自己定义的更新。
这里有一个奇怪的地方,3 次 setState 并没有合并在一起,而是触发了 2 次 react 更新,setPending(true) 为 1 次,setPending(false) 和 callback() 为第二次。
这是因为

ReactCurrentBatchConfig.transition = {}

这句语句将更新的上下文变更为了 transition。使得 setPending(true) 和 后面的 2 次更新的上下文不同了。

为什么更新的上下文变化会影响 setState 的合并呢,下面简单展开讲一讲 setState 时 react 在干什么。

WorkLoop

一次 react 更新,主核心的过程是 fiber tree 的协调(reconcile),协调的作用是找到 fiber tree 中发生变化的 fiber node,最小程度地对页面的 dom tree 结构进行调整。

在进行协调时,react 提供了两种模式:Legacy mode - 同步阻塞模式和 Concurrent mode - 并行模式。
这两种模式,区别在于 fiber tree 的协调过程是否可中断。 Legacy mode,协调过程不可中断;Concurrent mode,协调过程可中断。
Legacy mode:

legacy-mode.jpeg Concurrent mode:

Concurrent mode 的意义在于:

  • 协调不会长时间阻塞浏览器渲染;
  • 高优先级更新可以中断低优先级更新,优先渲染;

react 的调度机制是 workLoop 机制。伪代码实现如下:

let taskQueue = [];   // 任务列表
let shouldTimeEnd = 5ms;   // 一个时间片定义为 5ms
let channel = new MessageChannel();  // 创建一个 MessageChannel 实例function workLoop() {let beginTime = performance.now();  // 记录开始时间while(true) { // 循环处理 taskQueue 中的任务let currentTime = performance.now();  // 记录下一个任务开始时的时间if (currentTime - beginTime >= shouldTimeEnd) break;  // 时间片已经到期,结束任务处理processTask();  // 时间片没有到期,继续处理任务}if (taskQueue.length) { // 时间片到期,通过调用 postMessage,请求下一个时间片channel.port2.postMessage(null);}
}channel.port1.onmessage = workLoop;  // 在下一个时间片内继续处理任务
workLoop();

workLoop 有 2 种,Legacy 模式下,是 workLoopSync;Concurrent 模式下,是 workLoopConcurrent。workLoopSync 中每个任务都要完成后才会释放主进程,workLoopConcurrent 中每个任务在时间片耗尽后会释放主进程等待下一个时间片继续执行任务。

workLoopSync 对应 Legacy 模式。如果是在 event、setTimeout、network request 的 callback 中触发更新,那么协调时会启动 workLoopSync。在协调过程中,需要对 fiber tree 做深度优先遍历,处理每一个 fiber node。workLoopSync 开始工作以后,要等到 stack 中收集的所有 fiber node 都处理完毕以后,才会结束工作,也就是 fiber tree 的协调过程不可中断。

workLoopConcurrent 对应 Concurrent 模式。如果更新与 Suspense、useTransition、OffScreen 相关,那么协调时会启动 workLoopConcurrent。 workLoopConcurrent 开始工作以后,每次协调 fiber node 时,都会判断当前时间片是否到期。如果时间片到期,会停止当前 workLoopConcurrent、workLoop,让出主线程,然后请求下一个时间片继续协调。

相关源码如下:

参考 前端进阶面试题详细解答

function workLoopSync() {// Already timed out, so perform work without checking if we need to yield.while (workInProgress !== null) {performUnitOfWork(workInProgress);}
}function workLoopConcurrent() {// Perform work until Scheduler asks us to yieldwhile (workInProgress !== null && !shouldYield()) {performUnitOfWork(workInProgress);}
}

任务优先级

react 有 3 套优先级机制:

  • React 事件优先级
  • Scheduler 优先级
  • Lane 优先级

React 事件优先级如下:

// 离散事件优先级,例如:点击事件,input输入等触发的更新任务,优先级最高
export const DiscreteEventPriority: EventPriority = SyncLane;
// 连续事件优先级,例如:滚动事件,拖动事件等,连续触发的事件
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
// 默认事件优先级,例如:setTimeout触发的更新任务
export const DefaultEventPriority: EventPriority = DefaultLane;
// 闲置事件优先级,优先级最低
export const IdleEventPriority: EventPriority = IdleLane;

react 在内部定义了 5 种类型的调度(Scheduler)优先级:

  • ImmediatePriority, 直接优先级,对应用户的 click、input、focus 等操作;
  • UserBlockingPriority,用户阻塞优先级,对应用户的 mouseMove、scroll 等操作;
  • NormalPriority,普通优先级,对应网络请求、useTransition 等操作;
  • LowPriority,低优先级(未找到应用场景);
  • IdlePriority,空闲优先级,如 OffScreen;

5 种优先级的顺序为: ImmediatePriority > UserBlockingPriority > NormalPriority > LowPriority > IdlePriority。

react 内部定义了 31 条 lane,lane 可以理解为每个任务所处的赛道。用二进制表示,按优先级从低到高依次为:

lanes.png

lane 对应的位数越小,优先级最高。如 SyncLane 为 1,优先级最高; OffscreenLane 为 31, 优先级最低。

react 先将 lane 的优先级转换为 React 事件的优先级,然后再根据 React 事件的优先级转换为 Scheduler 的优先级。

当通过 startTransition 的方式触发更新时,更新对应的优先级等级为 NormalPriority。而在 NormalPriority 之上,还存在 ImmediatePriority 、UserBlockingPriority 这两种级别更高的更新。通常,高优先级的更新会优先级处理,这就使得尽管 transition 更新先触发,但并不会在第一时间处理,而是处于 pending - 等待状态。只有没有比 transition 更新优先级更高的更新存在时,它才会被处理。

Concurrent 模式下,如果在低优先级更新的协调过程中,有高优先级更新进来,那么高优先级更新会中断低优先级更新的协调过程。

每次拿到新的时间片以后,workLoopConcurrent 都会判断本次协调对应的优先级和上一次时间片到期中断的协调的优先级是否一样。如果一样,说明没有更高优先级的更新产生,可以继续上次未完成的协调;如果不一样,说明有更高优先级的更新进来,此时要清空之前已开始的协调过程,从根节点开始重新协调。等高优先级更新处理完成以后,再次从根节点开始处理低优先级更新

setState 机制

调用 setState,并不会立即更新组件 state。state 的更新,其实是发生在 fiber tree 的协调过程中,这个过程如下:

  1. 调用 setState
  2. 生成 update 对象:调用 setState 时传入的 new state 会存储在 update 对象的 payload 属性上
  3. 将 update 对象收集到 组件的 Fiber node 内部的 updateQueue 中
  4. 为更新创建 task:新建的 task 会添加到 taskQueue 堆顶
  5. workLoop 处理 task
  6. 协调 fiber tree
  7. 协调组件 fiber node
  8. 生成 new state:遍历 updateQueue 中所有的 update 对象,读取 payload 属性
  9. 执行组件 render
  10. fiber tree 协调完成
  11. 浏览器渲染

上面 useTransition 的例子中,连续 3 次 setState,会生成 3 个 update 对象 - update1(setPending(true)),update2(setPending(false)),update3(callback 里的 setState 调用)。这三个 update 对象会按照创建的先后顺序依次添加到 updateQueue 中。

update 对象结构:

export function createUpdate(eventTime: number, lane: Lane): Update<*> {const update: Update<*> = {eventTime,lane, // 这里为 update 绑定了优先级tag: UpdateState,payload: null,callback: null,next: null,};return update;
}

由于创建 update 对象的上下文不相同,导致 update 对象处理的时机不相同。第一次协调时,处理 update1;第二次协调时,处理 update2 和 update3。之所以这样,是因为不同的上下文,为 update 对象绑定了的不同的 lane。

lane 决定了 update 对象的处理时机。

所以如上,update1 被分配的 lane 为 InputContinuousLane,而 update2、update3 被分配的 lane 为 TransitionLane。为每个 update 生成 lane 的源码如下:

export function requestUpdateLane(fiber: Fiber): Lane {...const isTransition = requestCurrentTransition() !== NoTransition;if (isTransition) {if (currentEventTransitionLane === NoLane) {// All transitions within the same event are assigned the same lane.currentEventTransitionLane = claimNextTransitionLane();}return currentEventTransitionLane;}...
}export function requestCurrentTransition(): Transition | null {return ReactCurrentBatchConfig.transition;
}

至此,已经可以看到,update2 和 update3 被分配了较低的优先级,因此 3 次 setState 被分开成了 2 次更新。

了解了上面的原理,就可以来回答这几个问题了:

useTransition 为何能表现出 debounce 效果

高优先级更新会中断低优先级更新,优先处理。

startTransition 方法执行过程中, setPending(true) 触发的更新优先级较高,而 setPending(false)、callback 触发的更新优先级较低。当 callback 触发的更新进入协调阶段以后,由于协调过程可中断,并且用户一直在输入导致一直触发 setPending(true),使得 callback 触发的更新一直被中断,直到用户停止输入以后才能被完整处理。

useTransition 为何能表现出 throttle 效果

如果你一直输入,最多 5s,长列表必然会渲染,和 防抖 - throttle 效果一样。
这是因为为了防止低优先级更新一直被高优先级更新中断而得不到处理,react 为每种类型的更新定义了最迟必须处理时间 - timeout。如果在 timeout 时间内更新未被处理,那么更新的优先级就会被提升到最高 - ImmediatePriority,优先处理。

transition 更新的优先级为 NormalPriority,timeout 为 5000ms 即 5s。如果超过 5s, transition 更新还因为一直被高优先级更新中断而没有处理,它的优先级就会被提升为 ImmediatePriority,优先处理。这样就实现了 throttle 的效果。

useTransition 和 startTransition 区别

用户连续输入时,使用 useTransition 会出现 debounce 的效果,而直接使用 startTransition 则不会。

因为 startTransition 的源码:

function startTransition(scope) {var prevTransition = ReactCurrentBatchConfig.transition;ReactCurrentBatchConfig.transition = 1;  // 修改更新上下文try {scope();   // 触发更新} finally {...}
}

对比 useTransition 的 startTransition, 我们会发现 startTransition 中少了 setPending(true) 的过程。

使用 useTransition 时,transition 更新会一直被连续的 setPending(true) 中断,每次中断时都会被重置为未开始状态,导致 transition 更新只有在用户停止输入(或者超过 5s)时才能得到有效处理,也就出现了类似 debounce 的效果。

而直接使用 startTransition 时, 尽管协调过程会每隔 5ms 中断一次,但由于没有 setPending(true) 的中断, 连续的输入并不会重置 transition 更新。当 transition 更新结束协调时,自然而然地就会开始浏览器渲染过程,不会出现类似 debounce 的效果。

Transition API 由来

React 如何优化性能

React ,它本身的思路是纯 JS 写法,这种方式非常灵活,但是,这也使它在编译时很难做太多的事情,像上面这样的编译时优化是很难实现的。所以,我们可以看到 React 几个大版本的的优化主要都在运行时。
进行运行时优化,关注的主要问题就是 CPU 和 IO。

  • 首先,就是 CPU 的问题,主流浏览器的刷新频率一般是 60Hz,也就是每秒刷新 60 次,大概 16.6ms 浏览器刷新一次。由于 GUI 渲染线程和 JS 线程是互斥的,所以 JS 脚本执行和浏览器布局、绘制不能同时执行。在这 16.6ms 的时间里,浏览器既需要完成 JS 的执行,也需要完成样式的重排和重绘,如果 JS 执行的时间过长,超出了 16.6ms,这次刷新就没有时间执行样式布局和样式绘制了,于是在页面上就会表现为卡顿。
  • IO 的问题就比较好理解了,很多组件需要等待一些网络延迟,那么怎么样才能在网络延迟存在的情况下,减少用户对网络延迟的感知呢?就是 react 需要解决的问题。
    React 引入 fiber 机制,可中断协调阶段,就是在 CPU 角度优化运行时性能。而 Suspense API 则是 IO 角度的优化,让新内容替换成旧内容的过程不闪屏,内容切换更流畅。

Transition API 登场

Suspense 的作用,主要是 react 优化切换内容效果。而 Transition API 的最初提出,是为了配合 Suspense API 进行 IO 角度的优化。

useTransition 的前身是 withSuspenseConfig。Sebmarkbage 在 19 年五月份提的一个 PR 中引进了它。在 19 年 11 月更名为 useTransition。

Transition Hook 的作用是告诉 React,延迟更新 State 也没关系。

初版的 useTransition 的实现源码如下:

function updateTransition(config: SuspenseConfig | void | null,
): [(() => void) => void, boolean] {const [isPending, setPending] = updateState(false); // 相当于useStateconst startTransition = updateCallback(             // 相当于useCallbackcallback => {setPending(true); // 设置 pending 为 true// 以低优先级调度执行Scheduler.unstable_next(() => {// ⚛️ 设置suspenseConfigconst previousConfig = ReactCurrentBatchConfig.suspense;ReactCurrentBatchConfig.suspense = config === undefined ? null : config;try {// 还原 pendingsetPending(false);// 执行你的回调callback();} finally {// ⚛️ 还原suspenseConfigReactCurrentBatchConfig.suspense = previousConfig;}});},[config, isPending],);return [startTransition, isPending];
}

划重点,虽然跟现在的版本有一些差别,但主要的思想依然是:以较低的优先级运行后 2 次 setState。

一路以来,主要的修改包含:在做兼容数据流状态库如 redux,修改优先级的实现方案。

相关内容

热门资讯

TimesNet复现结果 复现环境:RTX3090,22312GB显存,torch&...
加密月解密:概述,基础篇 加密月解密:概述,基础篇 2022找工作是学历、能力和运气的超强结合体&...
天涯海角门票(三亚天涯海角攻略... 今天给各位分享天涯海角门票的知识,其中也会对三亚天涯海角攻略,天涯海角门票进行解释,如果能碰巧解决你...
狮子林门票(狮子林门票预约官网... 今天给各位分享狮子林门票的知识,其中也会对狮子林门票预约官网进行解释,如果能碰巧解决你现在面临的问题...
石家庄特价机票(石家庄特价机票... 今天给各位分享石家庄特价机票的知识,其中也会对石家庄特价机票99元进行解释,如果能碰巧解决你现在面临...
深圳到北京火车时刻表(去北京的... 今天给各位分享深圳到北京火车时刻表的知识,其中也会对去北京的火车时刻表进行解释,如果能碰巧解决你现在...
【SpringBoot】76、... JPA是Java Persistence API的简称,中文名Java持久层API&#...
LoRa烟雾报警器的安装方式 LoRa烟雾报警器是一种常用的消防安全设备,它能够在发现火灾或烟雾时迅速发出警报&#x...
如家快捷酒店上海(如家快捷酒店... 本篇文章极速百科给大家谈谈如家快捷酒店上海,以及如家快捷酒店上海人民广场对应的知识点,希望对各位有所...
梅河口酒店(梅河口酒店排行榜)... 本篇文章极速百科给大家谈谈梅河口酒店,以及梅河口酒店排行榜对应的知识点,希望对各位有所帮助,不要忘了...
宇豪海逸酒店(宇豪海逸酒店地址... 本篇文章极速百科给大家谈谈宇豪海逸酒店,以及宇豪海逸酒店地址对应的知识点,希望对各位有所帮助,不要忘...
温州金球国豪大酒店(温州金球国... 本篇文章极速百科给大家谈谈温州金球国豪大酒店,以及温州金球国豪大酒店餐饮预订电话对应的知识点,希望对...
[数据分析与可视化] Pyth... 本文主要介绍GeoPandas的基本使用方法,以绘制简单的地图。GeoPandas是一...
stm32H7内部flash存... 目录一、慎用固件库1.HAL_FLASH_Program有问题,写入不正常2.采用直接...
厦门环岛路客栈(厦门环岛路客栈... 本篇文章极速百科给大家谈谈厦门环岛路客栈,以及厦门环岛路客栈图片对应的知识点,希望对各位有所帮助,不...
欣燕都连锁酒店(欣燕都连锁酒店... 本篇文章极速百科给大家谈谈欣燕都连锁酒店,以及欣燕都连锁酒店北京前门店对应的知识点,希望对各位有所帮...
大连火车站附近旅店(大连火车站... 本篇文章极速百科给大家谈谈大连火车站附近旅店,以及大连火车站附近旅店哪家有小姐对应的知识点,希望对各...
厦门至石家庄机票(厦门到石家庄... 今天给各位分享厦门至石家庄机票的知识,其中也会对厦门到石家庄机票查询进行解释,如果能碰巧解决你现在面...
Web系统优化实战 1. 课程介绍2. 系统优化原理 1.1. Web系统优化概述 1.1.1. 提高网站速度 1.1....
笨鸟学数据结构(绪论) 数据结构的定义按某种逻辑关系组织起来的一批数据,按一定的映象方式把它存放在计算机的存储...
经典卷积模型回顾24—利用模型... 模型剪枝是指在训练深度学习模型时将某些不重要的部分剪掉,以实现更快速、更高效的模型精度...
关于重庆通信学院的信息 重庆通... 本篇文章极速百科给大家谈谈重庆通信学院,以及对应的知识点,希望对各位有所帮助,不要忘了收藏本站喔。本...
新民到沈阳的火车(新民到沈阳的... 今天给各位分享新民到沈阳的火车的知识,其中也会对新民到沈阳的火车时间表和票价进行解释,如果能碰巧解决...
中国携程网(中国携程网上订票官... 本篇文章极速百科给大家谈谈中国携程网,以及中国携程网上订票官网对应的知识点,希望对各位有所帮助,不要...
全国各地旅游景点(全国各地旅游... 今天给各位分享全国各地旅游景点的知识,其中也会对全国各地旅游景点大全地图进行解释,如果能碰巧解决你现...
《MongoDB入门教程》第2... 本文将会介绍 MongoDB 复合索引的概念,以及如何创建复合索引。 复合索引 复合索...
Python 关键字globa... 变量作用域 一般在函数体外定义的变量成为全局变量,在函数内部定义的变量称为局部变量。 ...
力扣-《剑指offer》-链表... 目录 第一题:从尾到头打印链表 第二题:删除链表的节点 第三题ÿ...
上海七天快捷酒店(上海七天连锁... 本篇文章极速百科给大家谈谈上海七天快捷酒店,以及上海七天连锁酒店预订对应的知识点,希望对各位有所帮助...
临沂观塘温泉(临沂观塘温泉大酒... 今天给各位分享临沂观塘温泉的知识,其中也会对临沂观塘温泉大酒店进行解释,如果能碰巧解决你现在面临的问...