亲宝软件园·资讯

展开

React状态更新的优先级机制源码解析

goClient1992 人气:0

为什么需要优先级

优先级机制最终目的是为了实现高优先级任务优先执行,低优先级任务延后执行

实现这一目的的本质就是在低优先级任务执行时,有更高优先级任务进来的话,可以打断低优先级任务的执行。

同步模式下的react运行时

我们知道在同步模式下,从 setState 到 虚拟DOM遍历,再到真实DOM更新,整个过程都是同步执行且无法被中断的,这样可能就会出现一个问题 —— 用户事件触发的更新被阻塞。

什么是用户事件触发的更新被阻塞?如果 React 正在进行更新任务,此时用户触发了交互事件,且在事件回调中执行了 setState,在同步模式下,这个更新任务需要 等待 当前正在更新的任务完成之后,才会被执行。假如当前 React 正在进行的更新任务耗时比较久,用户事件触发的更新任务不能及时被执行,造成下个更新任务被阻塞,从而形成了卡顿。

这时候,我们就希望能够及时响应用户触发的事件,优先执行用户事件触发的更新任务,也就是我们说的异步模式

我们可以比较一下,同步模式下和异步模式(优先级机制)下更新任务执行的差异

import React from "react";
import "./styles.css";
export default class extends React.Component {
  constructor() {
    super();
    this.state = {
      list: new Array(10000).fill(1),
    };
    this.domRef = null;
  }
  componentDidMount() {
    setTimeout(() => {
      console.log("setTimeout 准备更新", performance.now());
      this.setState(
        {
          list: new Array(10000).fill(Math.random() * 10000),
          updateLanes: 16
        },
        () => {
          console.log("setTimeout 更新完毕", performance.now());
        }
      );
    }, 100);
    setTimeout(() => {
      this.domRef.click();
    }, 150);
  }
  render() {
    const { list } = this.state;
    return (
      <div
        ref={(v) => (this.domRef = v)}        className="App"        onClick={() => {          console.log("click 准备更新", performance.now());          this.setState(            { list: new Array(10000).fill(2), updateLanes: 1 },            () => {              console.log("click 更新完毕", performance.now());            }          );        }}      >        {list.map((i, index) => (          <h2 key={i + +index}>Hello {i}</h2>
        ))}      </div>
    );
  }
}

click事件 触发的更新,会比 setTimeout 触发的更新更优先执行,做到了及时响应用户事件,打断 setTimeout 更新任务(低优先级任务)的执行。

如何运用优先级机制优化react运行时

为了解决同步模式渲染下的缺陷,我们希望能够对 react 做出下面这些优化

确定不同场景下的调度优先级

看过 react 源码的小伙伴可能都会有一个疑惑,为什么源码里面有那么多优先级相关的单词??怎么区分他们呢?

其实在 react 中主要分为两类优先级,scheduler 优先级和 lane 优先级,lane优先级下面又派生出 event 优先级

lane优先级

可以用赛道的概念去理解lane优先级,lane优先级有31个,我们可以用31位的二进制值去表示,值的每一位代表一条赛道对应一个lane优先级,赛道位置越靠前,优先级越高

优先级十进制值二进制值赛道位置
NoLane000000000000000000000000000000000
SyncLane100000000000000000000000000000010
InputContinuousHydrationLane200000000000000000000000000000101
InputContinuousLane400000000000000000000000000001002
DefaultHydrationLane800000000000000000000000000010003
DefaultLane1600000000000000000000000000100004
TransitionHydrationLane3200000000000000000000000001000005
TransitionLane16400000000000000000000000010000006
TransitionLane212800000000000000000000000100000007
TransitionLane325600000000000000000000001000000008
TransitionLane451200000000000000000000010000000009
TransitionLane51024000000000000000000001000000000010
TransitionLane2048000000000000000000010000000000011
TransitionLane74096000000000000000000100000000000012
TransitionLane88192000000000000000001000000000000013
TransitionLane916384000000000000000010000000000000014
TransitionLane1032768000000000000000100000000000000015
TransitionLane1165536000000000000001000000000000000016
TransitionLane12131072000000000000010000000000000000017
TransitionLane13262144000000000000100000000000000000018
TransitionLane14524288000000000001000000000000000000019
TransitionLane151048576000000000010000000000000000000020
TransitionLane162097152000000000100000000000000000000021
RetryLane14194304000000001000000000000000000000022
RetryLane28388608000000010000000000000000000000023
RetryLane316777216000000100000000000000000000000024
RetryLane433554432000001000000000000000000000000025
RetryLane567108864000010000000000000000000000000026
SelectiveHydrationLane134217728000100000000000000000000000000027
IdleHydrationLane268435456001000000000000000000000000000028
IdleLane536870912010000000000000000000000000000029
OffscreenLane1073741824100000000000000000000000000000030

event优先级

EventPriority Lane数值
DiscreteEventPriority离散事件。click、keydown、focusin等,事件的触发不是连续,可以做到快速响应SyncLane1
ContinuousEventPriority连续事件。drag、scroll、mouseover等,事件的是连续触发的,快速响应可能会阻塞渲染,优先级较离散事件低InputContinuousLane4
DefaultEventPriority默认的事件优先级DefaultLane16
IdleEventPriority空闲的优先级IdleLane536870912

scheduler优先级

SchedulerPriorityEventPriority大于>17.0.2小于>17.0.2
ImmediatePriorityDiscreteEventPriority199
UserblockingPriorityUserblocking298
NormalPriorityDefaultEventPriority397
LowPriorityDefaultEventPriority496
IdlePriorityIdleEventPriority595
NoPriority 090

优先级间的转换

lane优先级 转 event优先级(参考 lanesToEventPriority 函数)

event优先级 转 scheduler优先级(参考 ensureRootIsScheduled 函数)

event优先级 转 lane优先级(参考 getEventPriority 函数)

优先级机制如何设计

说到优先级机制,我们可能马上能联想到的是优先级队列,其最突出的特性是最高优先级先出react 的优先级机制跟优先级队列类似,不过其利用了赛道的概念,配合位与运算丰富了队列的功能,比起优先级队列,读写速度更快,更加容易理解

设计思路

合并赛道

场景

运算过程

DefaultLane优先级为16,SyncLane优先级为1

16 | 1 = 17

17的二进制值为10001
16的二进制值为10000,1的二进制值为00001

释放赛道

场景

运算过程

17 & ~1 = 16
17的二进制值为10001

为什么用位非?
~1 = -2
2 的二进制是00010,-2的话符号位取反变为10010
10001和10010进行位与运算得到10000,也就是十进制的16

找出最高优先级赛道

场景

运算过程

17 & -17 = 1

17的二进制值为10001
-17的二进制值为00001
10001和00001进行位与运算得到1,也就是SyncLane

快速定位赛道索引

场景

运算过程

// 找出 DefaultLane 赛道索引
31 - Math.clz32(16) = 4

16的二进制值为10000
索引4对应的就是第五个赛道

Math.clz32是用来干什么的?

判断赛道是否被占用

异步模式下会存在高优先级任务插队的情况,此情况下 state 的计算方式会跟同步模式下**有些不同。

场景

我们 setState 之后并不是马上就会更新 state,而是会根据 setState 的内容生成一个 Update 对象,这个对象包含了更新内容、更新优先级等属性。

更新 state 这个动作是在 processUpdateQueue 函数里进行的,函数里面会判断 Update 对象的优先级所在赛道是否被占用,来决定是否在此轮任务中计算这个 Update 对象的 state

运算过程

运算公式
(1 & 16) == 16

1的二进制值为00001
16的二进制值为10000 
00001和10000进行位与运算得到0

如何将优先级机制融入React运行时

生成一个更新任务

生成任务的流程其实非常简单,入口就在我们常用的 setState 函数,先上图

setState 函数内部执行的就是 enqueueUpdate 函数,而 enqueueUpdate 函数的工作主要分为4步:

步骤一:获取本次更新的优先级

步骤一的工作是调用 requestUpdateLane 函数拿到此次更新任务的优先级

如果当前为非 concurrent 模式

如果当前为 concurrent 模式

总的来说,requestUpdateLane 函数的优先级选取判断顺序如下:

SyncLane  >>  TransitionLane  >>  UpdateLane  >>  EventLane

估计有很多小伙伴都会很困惑一个问题,为什么会有这么多获取优先级的函数,这里我整理了一下其他函数的职责

步骤二:创建 Update 对象

这里的代码量不多,其实就是将 setState 的参数用一个对象封装起来,留给 render 阶段用

function createUpdate(eventTime, lane) {
  var update = {
    eventTime: eventTime,
    lane: lane,
    tag: UpdateState,
    payload: null,
    callback: null,
    next: null
  };
  return update;
}

步骤三:关联优先级

在这里先解释两个概念,一个是 HostRoot,一个是 FiberRootNode

这里关联优先级主要执行了两个函数

markUpdateLaneFromFiberToRoot。该函数主要做了两个事情

markRootUpdated。该函数也是主要做了两个事情

由此可见,react 的优先级机制并不独立运行在每一个组件节点里面,而是依赖一个全局的 react 应用根节点去控制下面多个组件树的任务调度

优先级关联到这些Fiber节点有什么用?

先说说他们的区别

具体应用场景

步骤四:发起调度

调度里面最关键的一步,就是 ensureRootIsScheduled 函数的调用,该函数的逻辑就是由下面两大部分构成,高优先级任务打断低优先级任务饥饿任务问题

高优先级任务打断低优先级任务

该部分流程可以分为三部曲

cancelCallback

var existingCallbackNode = root.callbackNode;
var existingCallbackPriority = root.callbackPriority;
var newCallbackPriority = getHighestPriorityLane(nextLanes);
if (existingCallbackPriority === newCallbackPriority) {
    ...
    return;
}
if (existingCallbackNode != null) {
    cancelCallback(existingCallbackNode);
}
newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root)
);
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;

上面是 ensureRootIsScheduled 函数的一些代码片段,先对变量做解释

existingCallbackNode:当前 render 阶段正在进行的任务

existingCallbackPriority:当前 render 阶段正在进行的任务优先级

newCallbackPriority:此次调度优先级

这里会判断 existingCallbackPrioritynewCallbackPriority 两个优先级是否相等,如果相等,此次更新合并到当前正在进行的任务中。如果不相等,代表此次更新任务的优先级更高,需要打断当前正在进行的任务

如何打断任务?

下面是 performConcurrentWorkOnRoot 代码片段

...
var originalCallbackNode = root.callbackNode;
...
// 函数末尾
if (root.callbackNode === originalCallbackNode) {
    return performConcurrentWorkOnRoot.bind(null, root);
}
return null;

由上面 ensureRootIsScheduled 的代码片段可以知道,performConcurrentWorkOnRoot 函数是被 scheduleCallback 函数调度的,具体返回后的逻辑需要到 Scheduler 模块去找

pop(taskQueue)

var callback = currentTask.callback;
if (typeof callback === 'function') {
  ...
} else {
  pop(taskQueue);
}

上面是 Scheduler 模块里面 workLoop 函数的代码片段,currentTask.callback 就是 scheduleCallback 的第二个参数,也就是performConcurrentWorkOnRoot 函数

承接上个主题,如果 performConcurrentWorkOnRoot 函数返回了null,workLoop 内部就会执行 pop(taskQueue),将当前的任务从 taskQueue 中弹出。

低优先级任务重启

上一步中说道一个低优先级任务从 taskQueue 中被弹出。那高优先级任务执行完毕之后,如何重启回之前的低优先级任务呢?

关键是在 commitRootImpl 函数

var remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
markRootFinished(root, remainingLanes);
...
ensureRootIsScheduled(root, now());

markRootFinished 函数刚刚上面说了是释放已完成任务所占用的赛道,那也就是说未完成任务依然会占用其赛道,所以我们可以重新调用 ensureRootIsScheduled 发起一次新的调度,去重启低优先级任务的执行。我们可以看下重启部分的判断

var nextLanes = getNextLanes(
    root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
);
// 如果 nextLanes 为 NoLanes,就证明所有任务都执行完毕了
if (nextLanes === NoLanes) {
    ...
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    // 只要 nextLanes 为 NoLanes,就可以结束调度了
    return;
}
// 如果 nextLanes 不为 NoLanes,就代表还有任务未执行完,也就是那些被打断的低优先级任务
...

饥饿任务问题

上面说到,在高优先级任务执行完毕之后,低优先级任务就会被重启,但假设如果持续有高优先级任务持续进来,我的低优先级任务岂不是没有重启之日?

所以 react 为了处理解决饥饿任务问题,react 在 ensureRootIsScheduled 函数开始的时候做了以下处理:(参考markStarvedLanesAsExpired函数)

var lanes = pendingLanes;
while (lanes > 0) {
    var index = pickArbitraryLaneIndex(lanes);
    var lane = 1 << index;
    var expirationTime = expirationTimes[index];
    if (expirationTime === NoTimestamp) {
      if ((lane & suspendedLanes) === NoLanes || (lane & pingedLanes) !== NoLanes) {
        expirationTimes[index] = computeExpirationTime(lane, currentTime);
      }
    } else if (expirationTime <= currentTime) {
      root.expiredLanes |= lane;
    }
    lanes &= ~lane;
}

可以参考 render 阶段执行的函数 performConcurrentWorkOnRoot 中的代码片段

var exitStatus = shouldTimeSlice(root, lanes) && ( !didTimeout) ? 
                    renderRootConcurrent(root, lanes) : 
                    renderRootSync(root, lanes);

可以看到只要 shouldTimeSlice 只要返回 false,就会执行 renderRootSync,也就是以同步优先级进入 render 阶段。而 shouldTimeSlice 的逻辑也就是刚刚的 expiredLanes 属性相关

function shouldTimeSlice(root, lanes) {
  // 如果 expiredLanes 里面有东西,代表有饥饿任务
  if ((lanes & root.expiredLanes) !== NoLanes) {
    return false;
  }
  var SyncDefaultLanes = InputContinuousHydrationLane | 
                          InputContinuousLane | 
                          DefaultHydrationLane | 
                          DefaultLane;
  return (lanes & SyncDefaultLanes) === NoLanes;
}

总结

react 的优先级机制在源码中并不是一个独立的,解耦的模块,而是涉及到了react整体运行的方方面面,最后回归整理下优先级机制在源码中的使用,让大家对优先级机制有一个更加整体的认知。

加载全部内容

相关教程
猜你喜欢
用户评论