React 优先级管理 - Lane 模型

在上文的 Fiber 中,我们经常看到 lanes 相关的变量,是否有很多疑问🤔,这个优先级是怎么定义的?他是怎么做比较多?跟以往的Expiration Times的区别是什么?我们带着这个疑问一起往下看

React在提升用户交互体验时,较为重要的一点是优先响应用户交互触发的更新任务,其余不那么重要的任务要做出让步,我们把用户交互触发的任务称为高优先级任务 ,不那么重要的任务称为低优先级任务

前言

React 在 Concurrent 模式下,不同优先级任务的存在会导致一种现象:高优先级的任务可以打断低优先级任务的执行。另外,倘若低优先级任务一直被高优先级任务打断,那么低优先级任务就会过期(应该在某个时间执行但是被打断了),会被强制执行。

这涉及到两个问题:高优先级任务插队饥饿问题

React通过引Lanes模型来解决优先级相关的问题,Lanes模型相对于传统的Expiration Times模型主要有两大优势:

  1. Lanes将任务优先级的概念("A任务优先级是否高于任务B?")与任务批处理("A任务是否属于这组任务?")分离开来。(增加了"批"的概念)
  2. Lanes可以用单个32位二进制数据表示许多不同的任务线程

Lanes

Lane意思就是车道,我们类比于F1赛车一样的车道,有内圈外圈之分。Lane优先级指的是像赛车车道一样来表示优先级,即最里面的优先级最高,按位依次降低。

js 复制代码
// Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-timeline.
// If those values are changed that package should be rebuilt and redeployed.

export const TotalLanes = 31;

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

export const SyncHydrationLane: Lane = /*               */ 0b0000000000000000000000000000001;
// 同步 lane,比如 click 事件
export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000010;
export const SyncLaneIndex: number = 1;

// 用户连续输入 lane,比如 drag 事件
export const InputContinuousHydrationLane: Lane = /*    */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /*             */ 0b0000000000000000000000000001000;

// 默认 lane,比如 setTimeout 后设置
export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /*                     */ 0b0000000000000000000000000100000;

export const SyncUpdateLanes: Lane = enableUnifiedSyncLane
  ? SyncLane | InputContinuousLane | DefaultLane
  : SyncLane;

// useTransition、useDeferredValue 时
const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000000000001000000;
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111111111110000000;
const TransitionLane1: Lane = /*                        */ 0b0000000000000000000000010000000;
const TransitionLane2: Lane = /*                        */ 0b0000000000000000000000100000000;
const TransitionLane3: Lane = /*                        */ 0b0000000000000000000001000000000;
const TransitionLane4: Lane = /*                        */ 0b0000000000000000000010000000000;
const TransitionLane5: Lane = /*                        */ 0b0000000000000000000100000000000;
const TransitionLane6: Lane = /*                        */ 0b0000000000000000001000000000000;
const TransitionLane7: Lane = /*                        */ 0b0000000000000000010000000000000;
const TransitionLane8: Lane = /*                        */ 0b0000000000000000100000000000000;
const TransitionLane9: Lane = /*                        */ 0b0000000000000001000000000000000;
const TransitionLane10: Lane = /*                       */ 0b0000000000000010000000000000000;
const TransitionLane11: Lane = /*                       */ 0b0000000000000100000000000000000;
const TransitionLane12: Lane = /*                       */ 0b0000000000001000000000000000000;
const TransitionLane13: Lane = /*                       */ 0b0000000000010000000000000000000;
const TransitionLane14: Lane = /*                       */ 0b0000000000100000000000000000000;
const TransitionLane15: Lane = /*                       */ 0b0000000001000000000000000000000;

// 出错后重试 lane
const RetryLanes: Lanes = /*                            */ 0b0000011110000000000000000000000;
const RetryLane1: Lane = /*                             */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /*                             */ 0b0000000100000000000000000000000;
const RetryLane3: Lane = /*                             */ 0b0000001000000000000000000000000;
const RetryLane4: Lane = /*                             */ 0b0000010000000000000000000000000;

export const SomeRetryLane: Lane = RetryLane1;

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

const NonIdleLanes: Lanes = /*                          */ 0b0000111111111111111111111111111;

export const IdleHydrationLane: Lane = /*               */ 0b0001000000000000000000000000000;
// 空闲 lane
export const IdleLane: Lane = /*                        */ 0b0010000000000000000000000000000;

// 离屏 lane
export const OffscreenLane: Lane = /*                   */ 0b0100000000000000000000000000000;
export const DeferredLane: Lane = /*                    */ 0b1000000000000000000000000000000;

位运算符

在了解 lane 之前,我们需要学会位运算符

按位非运算符 ~:

对每一位执行 操作,也可以理解为取反码

ini 复制代码
 A = 0b1101
~A = 0b0010

按位与运算符 &

对每一位执行与(AND) 操作,只要对应位置均为 1 时,结果才为 1,否则为 0。

js 复制代码
    A = 0b1101
    B = 0b0101
A & B = 0b0101

按位或运算符 |

对每一位执行或(OR) 操作,只要对应位置有一个 1 时,结果就为 1。

js 复制代码
    A = 0b1101
    B = 0b0101
A | B = 0b1101

按位异或运算符 ^

对每一位执行异或(XOR) 操作,当对应位置有且只有一个 1 时,结果就为 1,否则为 0。

js 复制代码
    A = 0b1101
    B = 0b0101
A | B = 0b1000

学完这些基本概念,我们就来看看优先级的设计机制是什么样的。

优先级

js 复制代码
export function requestUpdateLane(fiber: Fiber): Lane {
  // Special cases
  const mode = fiber.mode;

  // 如果当前 Fiber 节点的模式不是并发模式,则返回 SyncLane
  if ((mode & ConcurrentMode) === NoMode) {
    return (SyncLane: Lane);
  } else if (
    // 判断是否在渲染阶段,并且正在进行根节点的渲染
    (executionContext & RenderContext) !== NoContext &&
    workInProgressRootRenderLanes !== NoLanes
  ) {
    return pickArbitraryLane(workInProgressRootRenderLanes);
  }

  const transition = requestCurrentTransition();
  if (transition !== null) {
    const actionScopeLane = peekEntangledActionLane();
    
    return actionScopeLane !== NoLane
      ? actionScopeLane
      : requestTransitionLane(transition);
  }

  const updateLane: Lane = (getCurrentUpdatePriority(): any);
  if (updateLane !== NoLane) {
    return updateLane;
  }
  
  // 这里是获取当前事件的优先级,通过 window.event.type 可以识别当前事件。
  const eventLane: Lane = (getCurrentEventPriority(): any);
  return eventLane;
}

每次执行一次更新的时候,都会去获取对应的lane,默认情况下,会调用getCurrentEventPriority方法

js 复制代码
export function getCurrentEventPriority(): EventPriority {
  const currentEvent = window.event;
  if (currentEvent === undefined) {
    return DefaultEventPriority;
  }
  
  return getEventPriority(currentEvent.type);
}

当 window.event 不存在时,返回的是默认的事件优先级,否则执行getEventPriority

js 复制代码
export function getEventPriority(domEventName: DOMEventName): EventPriority {
  switch (domEventName) {
    // Used by SimpleEventPlugin:
    case 'cancel':
    case 'click':
    case 'close':
    case 'contextmenu':
    case 'copy':
    case 'cut':
    case 'auxclick':
    case 'dblclick':
    case 'dragend':
    case 'dragstart':
    case 'drop':
    case 'focusin':
    case 'focusout':
    case 'input':
    case 'invalid':
    case 'keydown':
    case 'keypress':
    case 'keyup':
    case 'mousedown':
    case 'mouseup':
    case 'paste':
    case 'pause':
    case 'play':
    case 'pointercancel':
    case 'pointerdown':
    case 'pointerup':
    case 'ratechange':
    case 'reset':
    case 'resize':
    case 'seeked':
    case 'submit':
    case 'touchcancel':
    case 'touchend':
    case 'touchstart':
    case 'volumechange':
    // Used by polyfills: (fall through)
    case 'change':
    case 'selectionchange':
    case 'textInput':
    case 'compositionstart':
    case 'compositionend':
    case 'compositionupdate':
    // Only enableCreateEventHandleAPI: (fall through)
    case 'beforeblur':
    case 'afterblur':
    // Not used by React but could be by user code: (fall through)
    case 'beforeinput':
    case 'blur':
    case 'fullscreenchange':
    case 'focus':
    case 'hashchange':
    case 'popstate':
    case 'select':
    case 'selectstart':
      // 均为同步优先级
      return DiscreteEventPriority;
    case 'drag':
    case 'dragenter':
    case 'dragexit':
    case 'dragleave':
    case 'dragover':
    case 'mousemove':
    case 'mouseout':
    case 'mouseover':
    case 'pointermove':
    case 'pointerout':
    case 'pointerover':
    case 'scroll':
    case 'toggle':
    case 'touchmove':
    case 'wheel':
    // Not used by React but could be by user code: (fall through)
    case 'mouseenter':
    case 'mouseleave':
    case 'pointerenter':
    case 'pointerleave':
      // 均为连续事件优先级
      return ContinuousEventPriority;
    case 'message': {
      // 获取 scheduler callback 的优先级
      const schedulerPriority = getCurrentSchedulerPriorityLevel();
      switch (schedulerPriority) {
        case ImmediateSchedulerPriority:
          return DiscreteEventPriority;
        case UserBlockingSchedulerPriority:
          return ContinuousEventPriority;
        case NormalSchedulerPriority:
        case LowSchedulerPriority:
          // TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
          return DefaultEventPriority;
        case IdleSchedulerPriority:
          return IdleEventPriority;
        default:
          return DefaultEventPriority;
      }
    }
    default:
      return DefaultEventPriority;
  }
}
js 复制代码
export const DiscreteEventPriority: EventPriority = SyncLane;
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
export const DefaultEventPriority: EventPriority = DefaultLane;
export const IdleEventPriority: EventPriority = IdleLane;

可见不同的事件类型对应不同lane,也就是对应不同的优先级。

运算

  • Lane 是如何合并成 Lanes 的?

  • 怎么从 Lanes 中找出优先级最高的?

Lanes之间都是通过二进制运算的,如:

js 复制代码
// 合并两条 lanes
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
  return a | b;
}

// 移除 lanes
export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
  return set & ~subset;
}

需要更新状态时,使用 lanes & -lanes 从相同的 lanes 中找出优先级最高的 lane

js 复制代码
export function getHighestPriorityLane(lanes: Lanes): Lane {
  return lanes & -lanes;
}

lanes & -lanes可以简单理解为:获取二进制数中最后一个为1的位的值,如11100100中最后一个1是100,因此计算出来的值就是4。

饥饿赛道(Lane)

不同优先级的任务在入队时都会有一个过期时间挂在头上,这个过期时间一旦到了,那这个任务的优先级就会被提到和SyncLane一样。React把这些包含过了过期时间的任务的赛道称为饥饿赛道。在我们每次往队列中添加调度任务的时候都会去检查一下是否有任务的过期时间已经超了,有的话就会把那个赛道标为饥饿赛道,这上面的任务就拥有着SyncLane任务的权利,同步执行!

举个例子来讲,现在正在执行一个优先级为 A 的 A更新任务,然后又进来了一个优先级为 B 的 B任务,由于任务B的优先级高于 A,那么 nextLanes 取的是任务B的优先级,因此会打断任务A,执行 cancelCallback,然后开始任务B的调度 scheduleCallback。如果下一次任务C进来又比任务A优先级高,导致任务A又没有被执行,并且任务 A 已经达到了预定的过期时间,这个时候就会导致饥饿问题。解决办法就是执行 markStarvedLanesAsExpired 方法,将任务A标记为过期,这样下一次它的执行优先级就为最高了,也就能够得到执行。

🤔思考

Q:为什么经常听别人说 lane 优先级共有31个赛道但有32个优先级?

在React中,一个bit就代表一个赛道,共32个bit,由于最左边的bit需要用来表示正负号(取反),所以就只剩下31个bit,这就是为什么只有31个赛道。至于32个优先级怎么来的,那是因为当所有bit上都为0时,表示无优先级,这个无优先级也算一个优先级。

相关推荐
还是大剑师兰特39 分钟前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解40 分钟前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django
张张打怪兽1 小时前
css-50 Projects in 50 Days(3)
前端·css