React 19 Concurrent 模式下的更新机制解析

2025-03-23

React 19 的 Concurrent 模式引入了精细的优先级调度系统,使不同类型的更新能以不同的优先级和路径流经 React 的内部渲染管道。本文将深入探讨 React 19 中各种类型更新的处理机制,包括离散事件、连续事件、setTimeout 和 Promise 微任务,并结合源码分析其内部实现。

优先级系统概览

在深入具体更新类型前,我们需要理解 React 的优先级系统:

  1. 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;
    
  2. 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;
    
  3. 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>

更新流程与源码解析

  1. 事件触发阶段

    React 的事件系统识别点击事件并将其分配为 DiscreteEventPriority

  2. 优先级设置阶段

    // 在 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;
      }
    }
    
  3. 更新调度阶段

    离散事件优先级会被转换为 SyncLane,然后通过微任务进行调度:

    // 在 scheduleTaskForRootDuringMicrotask 中
    if (includesSyncLane(nextLanes)) {
      // 同步工作,不需要额外调度,会在微任务结束时处理
      root.callbackPriority = SyncLane;
      return SyncLane;
    }
    
  4. 渲染处理

    离散事件触发的更新会在当前事件循环的微任务队列中完成,确保用户动作得到即时响应。微任务通过 processRootScheduleInMicrotask 处理。

离散事件

连续事件更新(ContinuousEventPriority)

连续事件是指持续发生的用户交互,如鼠标移动、滚动等。

例子:鼠标移动事件

<div
  style={{ height: '100px', width: '100px', background: 'red' }}
  onMouseMove={() => {
    setCount(count + 1);
    setCount(count + 1);
  }}
></div>

更新流程与源码解析

  1. 事件分类

    鼠标移动被识别为 ContinuousEventPriority

  2. 优先级映射

    在调度器中,ContinuousEventPriority 被映射为 UserBlockingSchedulerPriority

    switch (lanesToEventPriority(nextLanes)) {
      case DiscreteEventPriority:
      case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingSchedulerPriority;
        break;
      // ...
    }
    
  3. 调度与执行

    连续事件虽然不会同步执行,但仍有较高优先级,通过调度器任务快速执行:

    const newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performWorkOnRootViaSchedulerTask.bind(null, root)
    );
    
  4. 批处理

    连续事件中的多个更新会被自动批处理,两个 setCount 调用只会触发一次渲染。

连续事件

SetTimeout 中的更新(DefaultEventPriority)

setTimeout 回调中的更新具有默认优先级,可以被高优先级任务打断。

例子:setTimeout 中的状态更新

<button
  onClick={() => {
    setTimeout(() => {
      setCount(count + 1);
      setCount(count + 1);
      console.log('setTimeout', count);
    }, 5);
  }}
>
  setTimeout
</button>

更新流程与源码解析

  1. 优先级确定

    setTimeout 回调中的更新默认使用 DefaultEventPriority

    // 在调用 setState 时会获取当前优先级
    export function requestUpdateLane(fiber: Fiber): Lane {
      // 非事件上下文中,使用默认优先级
      return eventPriorityToLane(resolveUpdatePriority());
    }
    
  2. 调度路径

    从源码中可以看到,这些更新先通过微任务进行批处理,再使用调度器进行实际渲染:

    // 为每个根节点调度合适的任务
    const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
    
    // 在 scheduleTaskForRootDuringMicrotask 中
    const schedulerPriorityLevel = convertPriorityLevel(nextLanes);
    const newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performWorkOnRootViaSchedulerTask.bind(null, root)
    );
    
  3. 批处理合并

    在 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 对于优先级的上下文也只在这个时间循环中生效,它的大概过程是:

  1. 事件处理函数开始执行(设置优先级为 DiscreteEventPriority)
  2. 创建 Promise 并同步 resolve
  3. 注册 then 回调(加入微任务队列)
  4. 事件处理函数执行完毕
  5. JavaScript 引擎检查微任务队列,执行 then 回调
  6. 此时 React 内部的优先级上下文仍然有效
  7. setCount 使用事件的优先级(DiscreteEventPriority)

定时器

不同更新路径的实际应用

在实际开发中,了解这些更新机制有助于我们优化应用性能:

  1. 离散事件(如点击):立即响应,高优先级,微任务路径
  2. 连续事件(如滚动):快速响应但可中断,高优先级,调度器路径
  3. setTimeout 更新:可被高优先级更新打断,默认优先级,调度器路径
  4. Promise 微任务更新:会根据实际情况走不同的路径

总结

React 19 的 Concurrent 模式通过精细的优先级系统和双路径调度机制(微任务和调度器任务),实现了对不同类型更新的智能处理。离散事件获得最高优先级确保即时响应,连续事件高优先级但可中断,而 setTimeout 等不在 React 上下文内更新则以默认优先级执行,可被更重要的交互打断。

这种设计使 React 应用能够保持高响应性,同时高效处理复杂的异步更新,为用户提供流畅的交互体验。