目录为什么需要优先级同步模式下的React运行时如何运用优先级机制优化react运行时确定不同场景下的调度优先级lane优先级event优先级scheduler优先级优先级间的转换优
优先级机制最终目的是为了实现高优先级任务优先执行,低优先级任务延后执行。
实现这一目的的本质就是在低优先级任务执行时,有更高优先级任务进来的话,可以打断低优先级任务的执行。
我们知道在同步模式下,从 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
中主要分为两类优先级,scheduler
优先级和 lane
优先级,lane
优先级下面又派生出 event
优先级
可以用赛道的概念去理解lane优先级,lane优先级有31个,我们可以用31位的二进制值去表示,值的每一位代表一条赛道对应一个lane优先级,赛道位置越靠前,优先级越高
优先级 | 十进制值 | 二进制值 | 赛道位置 |
---|---|---|---|
NoLane | 0 | 0000000000000000000000000000000 | 0 |
SyncLane | 1 | 0000000000000000000000000000001 | 0 |
InputContinuousHydrationLane | 2 | 0000000000000000000000000000010 | 1 |
InputContinuousLane | 4 | 0000000000000000000000000000100 | 2 |
DefaultHydrationLane | 8 | 0000000000000000000000000001000 | 3 |
DefaultLane | 16 | 0000000000000000000000000010000 | 4 |
TransitionHydrationLane | 32 | 0000000000000000000000000100000 | 5 |
TransitionLane1 | 64 | 0000000000000000000000001000000 | 6 |
TransitionLane2 | 128 | 0000000000000000000000010000000 | 7 |
TransitionLane3 | 256 | 0000000000000000000000100000000 | 8 |
TransitionLane4 | 512 | 0000000000000000000001000000000 | 9 |
TransitionLane5 | 1024 | 0000000000000000000010000000000 | 10 |
TransitionLane | 2048 | 0000000000000000000100000000000 | 11 |
TransitionLane7 | 4096 | 0000000000000000001000000000000 | 12 |
TransitionLane8 | 8192 | 0000000000000000010000000000000 | 13 |
TransitionLane9 | 16384 | 0000000000000000100000000000000 | 14 |
TransitionLane10 | 32768 | 0000000000000001000000000000000 | 15 |
TransitionLane11 | 65536 | 0000000000000010000000000000000 | 16 |
TransitionLane12 | 131072 | 0000000000000100000000000000000 | 17 |
TransitionLane13 | 262144 | 0000000000001000000000000000000 | 18 |
TransitionLane14 | 524288 | 0000000000010000000000000000000 | 19 |
TransitionLane15 | 1048576 | 0000000000100000000000000000000 | 20 |
TransitionLane16 | 2097152 | 0000000001000000000000000000000 | 21 |
RetryLane1 | 4194304 | 0000000010000000000000000000000 | 22 |
RetryLane2 | 8388608 | 0000000100000000000000000000000 | 23 |
RetryLane3 | 16777216 | 0000001000000000000000000000000 | 24 |
RetryLane4 | 33554432 | 0000010000000000000000000000000 | 25 |
RetryLane5 | 67108864 | 0000100000000000000000000000000 | 26 |
SelectiveHydrationLane | 134217728 | 0001000000000000000000000000000 | 27 |
IdleHydrationLane | 268435456 | 0010000000000000000000000000000 | 28 |
IdleLane | 536870912 | 0100000000000000000000000000000 | 29 |
OffscreenLane | 1073741824 | 1000000000000000000000000000000 | 30 |
EventPriority | Lane | 数值 | |
---|---|---|---|
DiscreteEventPriority | 离散事件。click、keydown、focusin等,事件的触发不是连续,可以做到快速响应 | SyncLane | 1 |
ContinuousEventPriority | 连续事件。drag、scroll、mouseover等,事件的是连续触发的,快速响应可能会阻塞渲染,优先级较离散事件低 | InputContinuousLane | 4 |
DefaultEventPriority | 默认的事件优先级 | DefaultLane | 16 |
IdleEventPriority | 空闲的优先级 | IdleLane | 536870912 |
SchedulerPriority | EventPriority | 大于>17.0.2 | 小于>17.0.2 |
---|---|---|---|
ImmediatePriority | DiscreteEventPriority | 1 | 99 |
UserblockingPriority | Userblocking | 2 | 98 |
NormalPriority | DefaultEventPriority | 3 | 97 |
LowPriority | DefaultEventPriority | 4 | 96 |
IdlePriority | IdleEventPriority | 5 | 95 |
NoPriority | 0 | 90 |
lane优先级 转 event优先级(参考 lanesToEventPriority
函数)
event优先级 转 scheduler优先级(参考 ensureRootIsScheduled
函数)
event优先级 转 lane优先级(参考 getEventPriority
函数)
说到优先级机制,我们可能马上能联想到的是优先级队列,其最突出的特性是最高优先级先出,react
的优先级机制跟优先级队列类似,不过其利用了赛道的概念,配合位与运算丰富了队列的功能,比起优先级队列,读写速度更快,更加容易理解
场景
运算过程
a | b
DefaultLane优先级为16,SyncLane优先级为1
16 | 1 = 17
17的二进制值为10001
16的二进制值为10000,1的二进制值为00001
场景
运算过程
a & ~b
17 & ~1 = 16
17的二进制值为10001为什么用位非?
~1 = -2
2 的二进制是00010,-2的话符号位取反变为10010
10001和10010进行位与运算得到10000,也就是十进制的16
场景
运算过程
a & -b
17 & -17 = 1
17的二进制值为10001
-17的二进制值为00001
10001和00001进行位与运算得到1,也就是SyncLane
场景
运算过程
// 找出 DefaultLane 赛道索引
31 - Math.clz32(16) = 416的二进制值为10000
索引4对应的就是第五个赛道
Math.clz32是用来干什么的?
Math.clz32
的值就能得到该赛道的索引异步模式下会存在高优先级任务插队的情况,此情况下 state
的计算方式会跟同步模式下**有些不同。
场景
我们 setState 之后并不是马上就会更新 state
,而是会根据 setState 的内容生成一个 Update
对象,这个对象包含了更新内容、更新优先级等属性。
更新 state
这个动作是在 processUpdateQueue
函数里进行的,函数里面会判断 Update
对象的优先级所在赛道是否被占用,来决定是否在此轮任务中计算这个 Update
对象的 state
Update
对象优先级和当前正在进行的任务相等,可以根据 Update
对象计算 state
并更新到 Fiber 节点的 memoizedState
属性上Update
对象优先级高,相应的这个低优先级的 Update
对象将暂不被计算state,留到下一轮低优先级任务被重启时再进行计算运算过程
(renderLanes & updateLanes) == updateLanes
运算公式
(1 & 16) == 161的二进制值为00001
16的二进制值为10000
00001和10000进行位与运算得到0
生成任务的流程其实非常简单,入口就在我们常用的 setState
函数,先上图
setState
函数内部执行的就是 enqueueUpdate
函数,而 enqueueUpdate
函数的工作主要分为4步:
Update
对象ensureRootIsScheduled
调度。步骤一的工作是调用 requestUpdateLane
函数拿到此次更新任务的优先级
如果当前为非 concurrent
模式
如果当前为 concurrent
模式
Suspend
、useTransition
、useDefferedValue
等特性。在 transition
类型的优先级中寻找空闲的赛道。transition
类型的赛道有 16 条,从第 1 条到第 16 条,当到达第 16 条赛道后,下一次 transition
类型的任务会回到第 1 条赛道,如此往复。getCurrentUpdatePriority
函数。获取当前更新优先级。如果不为 NoLane
就返回getCurrentEventPriority
函数。返回当前的事件优先级。如果没有事件产生,返回 DefaultEventPriority
总的来说,requestUpdateLane
函数的优先级选取判断顺序如下:
SyncLane >> TransitionLane >> UpdateLane >> EventLane
估计有很多小伙伴都会很困惑一个问题,为什么会有这么多获取优先级的函数,这里我整理了一下其他函数的职责
这里的代码量不多,其实就是将 setState 的参数用一个对象封装起来,留给 render 阶段用
function createUpdate(eventTime, lane) {
var update = {
eventTime: eventTime,
lane: lane,
tag: UpdateState,
payload: null,
callback: null,
next: null
};
return update;
}
在这里先解释两个概念,一个是 HostRoot
,一个是 FiberRootnode
HostRoot
:就是 ReactDOM.render
的第一个参数,组件树的根节点。HostRoot
可能会存在多个,因为 ReactDOM.render
可以多次调用FiberRootNode
:react 的应用根节点,每个页面只有一个 react 的应用根节点。可以从 HostRoot
节点的 stateNode
属性访问这里关联优先级主要执行了两个函数
markUpdateLaneFromFiberToRoot
。该函数主要做了两个事情
HostRoot
,也就是 ReactDOM.render
的根节点,也就是说没有父节点了,所以第二件事情没有做markRootUpdated
。该函数也是主要做了两个事情
由此可见,react
的优先级机制并不独立运行在每一个组件节点里面,而是依赖一个全局的 react
应用根节点去控制下面多个组件树的任务调度
将优先级关联到这些Fiber节点有什么用?
先说说他们的区别
HostRoot
的 lane 优先级具体应用场景
markRootFinished
函数。beginWork
流程里面,会有很多判断 childLanes
是否被占用的判断调度里面最关键的一步,就是 ensureRootIsScheduled
函数的调用,该函数的逻辑就是由下面两大部分构成,高优先级任务打断低优先级任务 和 饥饿任务问题
该部分流程可以分为三部曲
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
:此次调度优先级
这里会判断 existingCallbackPriority
和 newCallbackPriority
两个优先级是否相等,如果相等,此次更新合并到当前正在进行的任务中。如果不相等,代表此次更新任务的优先级更高,需要打断当前正在进行的任务
如何打断任务?
cancelCallback(existingCallbackNode)
,cancelCallback
函数就是将 root.callbackNode
赋值为nullperformConcurrentWorkOnRoot
函数会先把 root.callbackNode
缓存起来,在函数末尾会再判断 root.callbackNode
和开始缓存起来的值是否一样,如果不一样,就代表 root.callbackNode
被赋值为null了,有更高优先级任务进来。performConcurrentWorkOnRoot
返回值为null下面是 performConcurrentWorkOnRoot
代码片段
...
var originalCallbackNode = root.callbackNode;
...
// 函数末尾
if (root.callbackNode === originalCallbackNode) {
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
由上面 ensureRootIsScheduled
的代码片段可以知道,performConcurrentWorkOnRoot
函数是被 scheduleCallback
函数调度的,具体返回后的逻辑需要到 Scheduler
模块去找
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;
}
NoTimestamp
,如果是,且该赛道存在待执行的任务,则为该赛道初始化过期时间expiredLanes
,这样在下一轮 render 阶段就会以同步优先级调度当前 HostRoot
可以参考 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整体运行的方方面面,最后回归整理下优先级机制在源码中的使用,让大家对优先级机制有一个更加整体的认知。
(reconcile)
的下一个阶段是渲染 (renderer)
,也就是我们说的 commit 阶段,在此阶段末尾,会调用 ensureRootIsScheduled
发起一次新的调度,执行尚未完成的低优先级任务。(reconcile)
,同步优先级就是最高的优先级,不会被打断以上就是React状态更新的优先级机制源码解析的详细内容,更多关于React 状态更新优先级的资料请关注编程网其它相关文章!
--结束END--
本文标题: React状态更新的优先级机制源码解析
本文链接: https://www.lsjlt.com/news/171031.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
下载Word文档到电脑,方便收藏和打印~
2024-01-12
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0