React 19 Concurrent 模式下的更新机制解析
2025-03-23
React 19 的 Concurrent 模式引入了精细的优先级调度系统,使不同类型的更新能以不同的优先级和路径流经 React 的内部渲染管道。本文将深入探讨 React 19 中各种类型更新的处理机制,包括离散事件、连续事件、setTimeout 和 Promise 微任务,并结合源码分析其内部实现。
优先级系统概览
在深入具体更新类型前,我们需要理解 React 的优先级系统:
-
Lane:最基础的优先级表示,使用二进制位表示不同的优先级通道。源码
export const NoLane: Lane = 0b0000000000000000000000000000000; export const SyncLane: Lane = 0b0000000000000000000000000000001; export const InputContinuousLane: Lane = 0b0000000000000000000000000000100; export const DefaultLane: Lane = 0b0000000000000000000000000010000; export const IdleLane: Lane = 0b0100000000000000000000000000000;
-
EventPriority:事件优先级,是 Lane 的别名。映射关系如下:源码
export const NoEventPriority: EventPriority = NoLane; export const DiscreteEventPriority: EventPriority = SyncLane; export const ContinuousEventPriority: EventPriority = InputContinuousLane; export const DefaultEventPriority: EventPriority = DefaultLane; export const IdleEventPriority: EventPriority = IdleLane;
-
SchedulerPriority:调度器优先级,控制任务在调度器中的执行顺序。 源码
export const NoPriority = 0; // 最高优先级,立即执行,超时值为 -1(实际上表示立即过期) export const ImmediatePriority = 1; // 用户阻塞优先级,用于用户交互相关的更新,超时值 userBlockingPriorityTimeout = 250 export const UserBlockingPriority = 2; // 普通优先级,超时值 normalPriorityTimeout = 5000 export const NormalPriority = 3; // 低优先级,超时值 lowPriorityTimeout = 10000 export const LowPriority = 4; // 最低优先级,仅在空闲时执行,超时值 maxSigned31BitInt = 1073741823 // Max 31 bit integer. The max integer size in V8 for 32-bit systems. // Math.pow(2, 30) - 1 // 0b111111111111111111111111111111 export const IdlePriority = 5;
React 会根据当前的上下文来决定每个更新的优先级,它主要有两种更新模式: 微任务更新和调度器更新(performWorkOnRootViaSchedulerTask)。对于高优先级的任务(如点击事件触发的更新优先级是 DiscreteEventPriority)会在微任务中同步更新;对于优先级不高的任务(如 setTimeout、mouseMove 触发的更新),则会通过 Scheduler 进行调度更新。
如果需要通过 Scheduler 更新,需要先把 Lane 转为 Scheduler 的优先级:
let schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}
离散事件更新(DiscreteEventPriority)
离散事件是指用户产生的不连续交互,如点击、键盘输入等。
例子:点击事件
<button
onClick={() => {
setCount(count + 2);
}}
>
Click me
</button>
更新流程与源码解析
-
事件触发阶段:
React 的事件系统识别点击事件并将其分配为
DiscreteEventPriority
。 -
优先级设置阶段:
// 在 ReactDOMEventListener.js 中 function dispatchDiscreteEvent( domEventName, eventSystemFlags, container, nativeEvent ) { const prevTransition = ReactSharedInternals.T; ReactSharedInternals.T = null; const previousPriority = getCurrentUpdatePriority(); try { // 设置为最高优先级 setCurrentUpdatePriority(DiscreteEventPriority); dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent); } finally { setCurrentUpdatePriority(previousPriority); ReactSharedInternals.T = prevTransition; } }
-
更新调度阶段:
离散事件优先级会被转换为
SyncLane
,然后通过微任务进行调度:// 在 scheduleTaskForRootDuringMicrotask 中 if (includesSyncLane(nextLanes)) { // 同步工作,不需要额外调度,会在微任务结束时处理 root.callbackPriority = SyncLane; return SyncLane; }
-
渲染处理:
离散事件触发的更新会在当前事件循环的微任务队列中完成,确保用户动作得到即时响应。微任务通过
processRootScheduleInMicrotask
处理。
连续事件更新(ContinuousEventPriority)
连续事件是指持续发生的用户交互,如鼠标移动、滚动等。
例子:鼠标移动事件
<div
style={{ height: '100px', width: '100px', background: 'red' }}
onMouseMove={() => {
setCount(count + 1);
setCount(count + 1);
}}
></div>
更新流程与源码解析
-
事件分类:
鼠标移动被识别为
ContinuousEventPriority
。 -
优先级映射:
在调度器中,
ContinuousEventPriority
被映射为UserBlockingSchedulerPriority
:switch (lanesToEventPriority(nextLanes)) { case DiscreteEventPriority: case ContinuousEventPriority: schedulerPriorityLevel = UserBlockingSchedulerPriority; break; // ... }
-
调度与执行:
连续事件虽然不会同步执行,但仍有较高优先级,通过调度器任务快速执行:
const newCallbackNode = scheduleCallback( schedulerPriorityLevel, performWorkOnRootViaSchedulerTask.bind(null, root) );
-
批处理:
连续事件中的多个更新会被自动批处理,两个
setCount
调用只会触发一次渲染。
SetTimeout 中的更新(DefaultEventPriority)
setTimeout 回调中的更新具有默认优先级,可以被高优先级任务打断。
例子:setTimeout 中的状态更新
<button
onClick={() => {
setTimeout(() => {
setCount(count + 1);
setCount(count + 1);
console.log('setTimeout', count);
}, 5);
}}
>
setTimeout
</button>
更新流程与源码解析
-
优先级确定:
setTimeout 回调中的更新默认使用
DefaultEventPriority
:// 在调用 setState 时会获取当前优先级 export function requestUpdateLane(fiber: Fiber): Lane { // 非事件上下文中,使用默认优先级 return eventPriorityToLane(resolveUpdatePriority()); }
-
调度路径:
从源码中可以看到,这些更新先通过微任务进行批处理,再使用调度器进行实际渲染:
// 为每个根节点调度合适的任务 const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime); // 在 scheduleTaskForRootDuringMicrotask 中 const schedulerPriorityLevel = convertPriorityLevel(nextLanes); const newCallbackNode = scheduleCallback( schedulerPriorityLevel, performWorkOnRootViaSchedulerTask.bind(null, root) );
-
批处理合并:
在 React 18+ 中,setTimeout 中的多个 setState 会自动批处理:
// 这两个更新会被批处理为一个渲染 setCount(count + 1); setCount(count + 1);
然而,由于闭包特性,两个更新使用相同的 count 值,最终只会+1 而不是+2。
Promise(微任务)中的更新
Promise 回调中的更新同样具有默认优先级,但执行时机不同。
例子:Promise 微任务中的状态更新
<button
onClick={() => {
Promise.resolve().then(() => {
setCount(count + 1);
setCount(count + 1);
});
}}>
Promise
</button>
<button
onClick={() => {
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
}).then(() => {
setCount(count + 1);
setCount(count + 1);
});
}}>
PromiseWithSetTimeout
</button>
更新流程与源码解析
第一个按钮的更新流程和上面点击事件更新的流程是一样的,优先级同样是 DiscreteEventPriority。
第二个按钮则同 setTimeout 例子中一样,优先级是 DefaultEventPriority。
这是因为第一个按钮中,setCount 的调用在当前的事件循环中,而 react 对于优先级的上下文也只在这个时间循环中生效,它的大概过程是:
- 事件处理函数开始执行(设置优先级为 DiscreteEventPriority)
- 创建 Promise 并同步 resolve
- 注册 then 回调(加入微任务队列)
- 事件处理函数执行完毕
- JavaScript 引擎检查微任务队列,执行 then 回调
- 此时 React 内部的优先级上下文仍然有效
- setCount 使用事件的优先级(DiscreteEventPriority)
不同更新路径的实际应用
在实际开发中,了解这些更新机制有助于我们优化应用性能:
- 离散事件(如点击):立即响应,高优先级,微任务路径
- 连续事件(如滚动):快速响应但可中断,高优先级,调度器路径
- setTimeout 更新:可被高优先级更新打断,默认优先级,调度器路径
- Promise 微任务更新:会根据实际情况走不同的路径
总结
React 19 的 Concurrent 模式通过精细的优先级系统和双路径调度机制(微任务和调度器任务),实现了对不同类型更新的智能处理。离散事件获得最高优先级确保即时响应,连续事件高优先级但可中断,而 setTimeout 等不在 React 上下文内更新则以默认优先级执行,可被更重要的交互打断。
这种设计使 React 应用能够保持高响应性,同时高效处理复杂的异步更新,为用户提供流畅的交互体验。