关于React的并发模式(Concurrent Mode)总结

584 阅读8分钟

1. 概览

Concurrent 模式是一组 React 的新功能,可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整。

Concurrent Mode是React16年重构 Fiber架构 的源动力,也是React未来的发展方向。

1.1 底层架构--fiber架构

Concurrent Mode最关键的一点是:实现异步可中断的更新。

Fiber架构的意义在于,他将单个组件作为工作单元,使以组件为粒度的“异步可中断的更新”成为可能。

1.2 架构的驱动力--Scheduler

当同步运行Fiber架构(通过ReactDOM.render),则Fiber架构与V15并无区别。

但是当我们配合时间切片,就能根据宿主环境性能,为每个工作单元分配一个可运行时间,实现“异步可中断的更新”

1.3 架构运行策略--lane

基于当前的架构,当一次更新在运行过程中被中断,过段时间再继续运行,这就是“异步可中断的更新”;

当一次更新在运行过程中被中断,转而重新开始一次新的更新,我们可以说:后一次更新打断了前一次更新;

这就是优先级的概念:后一次更新的优先级更高,就会打断正在进行的前一次更新。

多个优先级之间如何互相打断?优先级能否升降?本次更新应该赋予什么优先级?

这就是需要一个模型控制不同优先级之间的关系与行为,也就是lane。

1.4 上层实现

从源码层面讲,Concurrent Mode是一套可控的“多优先级更新架构”,落地的上层实现包括:

  1. batchUpdates
  2. Suspense
  3. useDeferredValue

2. scheduler 原理及实现

scheduler实现功能:

  1. 时间切片
  2. 优先级调度

2.1 时间切片原理

时间切片的本质是模拟实现requestIdleCallback

除去“浏览器重排/重绘”,下图是浏览器一帧中可以用于执行JS的时机。

一个task(宏任务) -- 队列中全部job(微任务) -- requestAnimationFrame -- 浏览器重排/重绘 -- requestIdleCallback

requestIdleCallback是在“浏览器重排/重绘”后如果当前帧还有空余时间时被调用的。

浏览器并没有提供其他API能够在同样的时机(浏览器重排/重绘后)调用以模拟其实现。

唯一能精准控制调用时机的API是requestAnimationFrame,他能让我们在“浏览器重排/重绘”之前执行JS。

这也是为什么我们通常用这个API实现JS动画 —— 这是浏览器渲染前的最后时机,所以动画能快速被渲染。

所以,Scheduler的时间切片功能是通过task(宏任务)实现的。

  • setTimeout:最常见
  • MessageChannel:执行时机比setTimeout更早

所以Scheduler将需要被执行的回调函数作为MessageChannel的回调执行。如果当前宿主环境不支持MessageChannel,则使用setTimeout

在React的render阶段,开启Concurrent Mode时,每次遍历前,都会通过Scheduler提供的shouldYield方法判断是否需要中断遍历,使浏览器有时间渲染:

function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

在Schdeduler中,为任务分配的初始剩余时间为5ms,但随着应用运行,会通过fps动态调整分配给任务的可执行时间。

 forceFrameRate = function(fps) {
    if (fps < 0 || fps > 125) {
      // Using console['error'] to evade Babel and ESLint
      console['error'](
        'forceFrameRate takes a positive int between 0 and 125, ' +
          'forcing frame rates higher than 125 fps is not unsupported',
      );
      return;
    }
    if (fps > 0) {
      yieldInterval = Math.floor(1000 / fps);
    } else {
      // reset the framerate
      yieldInterval = 5;
    }
  };

这也解释了为什么在设置Concurrent Mode后每个任务的执行时间大体都是多于5ms的一小段时间 —— 每个时间切片被设定为5ms,任务本身再执行一小段时间,所以整体时间是多于5ms的时间.

2.2 优先级调度

Scheduler是独立于React的包,对外暴露了一个方法unstable_runWithPriority

这个方法接受一个优先级与一个回调函数,在回调函数内部调用获取优先级的方法都会取得第一个参数对应的优先级:

function unstable_runWithPriority(priorityLevel, eventHandler) {
  switch (priorityLevel) {
    case ImmediatePriority:
    case UserBlockingPriority:
    case NormalPriority:
    case LowPriority:
    case IdlePriority:
      break;
    default:
      priorityLevel = NormalPriority;
  }

  var previousPriorityLevel = currentPriorityLevel;
  currentPriorityLevel = priorityLevel;

  try {
    return eventHandler();
  } finally {
    currentPriorityLevel = previousPriorityLevel;
  }
}

一共有5种优先级

// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;

3. Lane模型

lane模型就是react优先级的机制,可以用来

  • 可以表示优先级的不同
  • 可能同时存在几个同优先级的更新,所以还得能表示批的概念
  • 方便进行优先级相关计算

3.1 表示优先级不同

lane模型使用31位的二进制表示31条赛道,位数越小的优先级越高,某些相邻的位拥有相同优先级。

export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;

export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;
export const SyncBatchedLane: Lane = /*                 */ 0b0000000000000000000000000000010;

export const InputDiscreteHydrationLane: Lane = /*      */ 0b0000000000000000000000000000100;
const InputDiscreteLanes: Lanes = /*                    */ 0b0000000000000000000000000011000;

const InputContinuousHydrationLane: Lane = /*           */ 0b0000000000000000000000000100000;
const InputContinuousLanes: Lanes = /*                  */ 0b0000000000000000000000011000000;

export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000100000000;
export const DefaultLanes: Lanes = /*                   */ 0b0000000000000000000111000000000;

const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000001000000000000;
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111110000000000000;

const RetryLanes: Lanes = /*                            */ 0b0000011110000000000000000000000;

export const SomeRetryLane: Lanes = /*                  */ 0b0000010000000000000000000000000;

export const SelectiveHydrationLane: Lane = /*          */ 0b0000100000000000000000000000000;

const NonIdleLanes = /*                                 */ 0b0000111111111111111111111111111;

export const IdleHydrationLane: Lane = /*               */ 0b0001000000000000000000000000000;
const IdleLanes: Lanes = /*                             */ 0b0110000000000000000000000000000;

export const OffscreenLane: Lane = /*                   */ 0b1000000000000000000000000000000;

同步优先级占用的位数为第一位

export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;

3.2 表示“批”的概念

const InputDiscreteLanes: Lanes = /*                    */ 0b0000000000000000000000000011000;
export const DefaultLanes: Lanes = /*                   */ 0b0000000000000000000111000000000;
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111110000000000000;

其中的某些变量占了多个位,这就是批

其中InputDiscreteLanes是“用户交互”触发更新会拥有的优先级范围。

DefaultLanes是“请求数据返回后触发更新”拥有的优先级范围。

TransitionLanes是Suspense、useTransition、useDeferredValue拥有的优先级范围。

这其中有个细节,越低优先级的lanes占用的位越多。比如InputDiscreteLanes占了2个位,TransitionLanes占了9个位。

原因在于:越低优先级的更新越容易被打断,导致积压下来,所以需要更多的位。相反,最高优的同步更新的SyncLane不需要多余的lanes

3.3 方便进行优先级相关计算

// 判断a b是否有交集
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
  return (a & b) !== NoLanes;
}

// 计算b这个lanes是否是a对应的lanes的子集,只需要判断a与b按位与的结果是否为b:
export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane) {
  return (set & subset) === subset;
}

// 将两个lane或lanes的位合并只需要执行按位或操作:
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
  return a | b;
}