剖析React系列十三-react调度
本系列是讲述从0开始实现一个react18的基本版本。通过实现一个基本版本,让大家深入了解React
内部机制。
由于React
源码通过Mono-repo 管理仓库,我们也是用pnpm
提供的workspaces
来管理我们的代码仓库,打包我们使用rollup
进行打包。
系列文章:
- React实现系列一 - jsx
- 剖析React系列二-reconciler
- 剖析React系列三-打标记
- 剖析React系列四-commit
- 剖析React系列五-update流程
- 剖析React系列六-dispatch update流程
- 剖析React系列七-事件系统
- 剖析React系列八-同级节点diff
- 剖析React系列九-Fragment的部分实现逻辑
- 剖析React系列十- 调度<合并更新、优先级>
- 剖析React系列十一- useEffect的实现原理
- 剖析React系列十二-调度器的实现
上一讲中,我们讲解了关于scheduler
包的源码相关的知识点,还通过一个例子讲解了高优先级分片执行,或者高优先级打断低优先级的情况。这一节我们主要是结合react
的代码,讲解scheduler
是如何和react
相结合的。
开始之前我们首先要明白一个基本知识,在react
中,render
阶段是通过调度可以中断的,但是commit
阶段是不可以中断的。
所以我们接下来讲解的调度
都是用来中断render
(调和)阶段。
调度入口scheduleUpdateOnFiber
不管是首页渲染,还是我们通过useState
触发的更新操作,react
内部都会走到scheduleUpdateOnFiber
中,开始调度执行。
scheduleUpdateOnFiber
中主要功能是更新可能从不同的fiberNode
中触发。但是react
的每次更新都是从根节点开始。所以scheduleUpdateOnFiber
就是向上遍历到根节点。
export function scheduleUpdateOnFiber(fiber: FiberNode, lane: Lane) {
// fiberRootNode
let root = markUpdateFromFiberToRoot(fiber);
markRootUpdated(root, lane);
ensureRootIsScheduled(root);
}
开始调度ensureRootIsScheduled
我们真正的调度开始逻辑是在ensureRootIsScheduled
中,它会比较不同的优先级,然后实现不同的逻辑。
主要逻辑分为这几个步骤:
- 获取当前任务的最高优先级
- 对比上一个优先级和此次优先级,如果相同就退出执行。如果不同就证明此次比上一次高,取消上一次正在执行的任务
- 判断是任务的优先级是不是同步执行的任务,如果是同步执行的话,就通过微任务批量执行。如果不是的话,就通过宏任务开启调度。
它主要是使用了我们上一讲scheduler
中的如下方法:
unstable_cancelCallback
: 取消正在执行的任务scheduleCallback
: 调度器的开始工作
/**
* schedule调度阶段入口
*/
function ensureRootIsScheduled(root: FiberRootNode) {
let updateLane = getHighestPriorityLane(root.pendingLanes);
// 获取当前的callback
const existingCallback = root.callbackNode;
if (updateLane === NoLane) {
if (existingCallback !== null) {
unstable_cancelCallback(existingCallback);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
const curPriority = updateLane;
const prevPriority = root.callbackPriority;
if (curPriority === prevPriority) {
// 如果之前的优先级等于当前的优先级, 不需要重新的调度,scheduler会自动的获取performConcurrentWorkOnRoot的返回函数继续调度
// (return performConcurrentWorkOnRoot.bind(null, root); 中继续调度)
return;
}
// 当前产生了更高优先级调度,取消之前的调度
if (existingCallback !== null) {
unstable_cancelCallback(existingCallback);
}
let newCallbackNode = null;
if (updateLane === SyncLane) {
// 同步优先级 用微任务调度
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root, updateLane));
scheduleMicroTask(flushSyncCallbacks);
} else {
// 其他优先级 用宏任务调度
// 将react-lane 转换成 调度器的优先级
const schedulerPriority = lanesToSchedulerPriority(updateLane);
newCallbackNode = scheduleCallback(
schedulerPriority,
// @ts-ignore
performConcurrentWorkOnRoot.bind(null, root)
);
}
// 保存当前的调度任务以及调度任务的优先级
root.callbackNode = newCallbackNode;
root.callbackPriority = curPriority;
}
我们接下来看看performConcurrentWorkOnRoot
内部执行的逻辑。
调度performConcurrentWorkOnRoot
performConcurrentWorkOnRoot
是scheduler
真正执行的函数。也是react
包和scheduler
包相交的入口,是时间分片的关键地方。
它接受一个scheduler
传递的任务是否超时的参数didTimeout
,如果任务超时,就同步执行超时的任务。可以用户解决饥饿问题(一个任务一直没有被执行)。
主要逻辑是:
- 根据任务的优先级和是否超时,判断是否同步执行
needSync
- 调用
render
阶段真正的调和函数renderRoot
- 重新执行调度
ensureRootIsScheduled
,用于并发更新中断后,继续调度。(上一讲中调度函数返回一个函数继续执行) - 如果调和完成,开始进入
commit
阶段
/**
* 并发更新的render入口 -> scheduler时间切片执行的函数
* @didTimeout: 调度器传入 -> 任务是否过期
*/
function performConcurrentWorkOnRoot(
root: FiberRootNode,
didTimeout: boolean
): any {
// 并发开始的时候,需要保证useEffect回调已经执行
// 因为useEffect的执行会触发更新,可能产生更高优先级的更新。
// function App() {
// useEffect(() => {
// updatexxx() // 如果触发了更高级别的更新
// }, [])
// }
const curCallback = root.callbackNode;
let didFlushPassiveEffect = flushPassiveEffects(root.pendingPassiveEffects);
if (didFlushPassiveEffect) {
// 这里表示:useEffect执行,触发了更新,并产生了比当前的更新优先级更高的更新,取消本次的调度
if (root.callbackNode !== curCallback) {
return null;
}
}
const lane = getHighestPriorityLane(root.pendingLanes);
const curCallbackNode = root.callbackNode;
// 防御性编程
if (lane === NoLane) {
return null;
}
const needSync = lane === SyncLane || didTimeout;
// render阶段
const exitStatus = renderRoot(root, lane, !needSync);
// 再次执行调度,用于判断之后root.callbackNode === curCallbackNode,
// 因为如果并发过程中,优先级没有变,在执行调度后,由于curPriority === prevPriority,直接返回,导致curCallbackNode相等,继续调度
// 如果有更高优先级的调度的话,本次调度直接返回null,停止调度
ensureRootIsScheduled(root);
// 中断
if (exitStatus === RootInComplete) {
// ensureRootIsScheduled中有更高的优先级插入进来, 停止之前的调度
if (root.callbackNode !== curCallbackNode) {
return null;
}
// 继续调度
return performConcurrentWorkOnRoot.bind(null, root);
}
// 已经更新完
if (exitStatus === RootCompleted) {
const finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
root.finishedLane = lane;
wipRootRenderLane = NoLane;
commitRoot(root);
}
}
从上面可以看出,内部调用了renderRoot
方法,然后根据renderRoot
方法返回的状态(1. 中断。2. 执行完)判断不同的逻辑。我们看看renderRoot
是如何返回任务是否执行完成的。
render真正执行的位置renderRoot
它的重要逻辑就是根据我们传入的优先级,判断是否是同步还是并发的执行任务调度。
以及根据调度的状态返回是否是调度完成,还是由于时间分片被中断。
/**
* 并发和同步更新的入口(render阶段)
* @param root
* @param lane
* @param shouldTimeSlice
*/
function renderRoot(root: FiberRootNode, lane: Lane, shouldTimeSlice: boolean) {
if (__DEV__) {
console.log(`开始${shouldTimeSlice ? "并发" : "同步"}render更新`);
}
// 由于并发更新会不断的执行,但是并不需要更新,所以我们需要判断优先级看看是否需要初始化
// 如果wipRootRenderLane 不等于 当前更新的lane, 就需要重新初始化,从根部开始调度
if (wipRootRenderLane !== lane) {
// 初始化,将workInProgress 指向第一个fiberNode
prepareFreshStack(root, lane);
}
do {
try {
shouldTimeSlice ? workLoopConcurrent() : workLoopSync();
break;
} catch (e) {
workInProgress = null;
}
} while (true);
// 中断执行
if (shouldTimeSlice && workInProgress !== null) {
return RootInComplete;
}
if (!shouldTimeSlice && workInProgress !== null && __DEV__) {
console.error(`render阶段结束时wip不应该为null`);
}
//render阶段执行完
return RootCompleted;
}
从上面的代码,我们可以看出几个点:.
prepareFreshStack
这个函数标记着是否需要从根fiberRootNode
开始执行。这里很重要,用于并发被中断后,不从头部执行,而是继续上一次的调度。- 根据传入的
shouldTimeSlice
来判断执行的workLoop
方法 - 返回
中断RootInComplete
或者完成RootCompleted
workLoopConcurrent
和workLoopSync
从名字中,我们可以猜出大概的意思了。
workLoopSync
: 代表同步执行,不会被中断。workLoopConcurrent
: 时间分片,可以被中断, 通过调度器scheduler
提供的方法,判断时间是否用完。如果用完就停止循环,将主动权返回给浏览器,等待下一次执行。
// 同步更新
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// 并发更新
function workLoopConcurrent() {
while (workInProgress !== null && !unstable_shouldYield()) {
performUnitOfWork(workInProgress);
}
}
总结
上面我们大致的看了一下代码的执行流程,接下来,我们来通过几个例子来讲解执行的流程。
例子 - 同一个任务时间分片
假如我们刚刚开始渲染页面的时候,默认是普通的优先级,所以会使用时间分片去切割一个大的任务。通过上面的主流程分析的大致应该明白了整个执行过程。
- 刚刚开始会进入
scheduleUpdateOnFiber
, 去开始调度。 ensureRootIsScheduled
进入,获取本次的优先级,例如普通优先级- 调用调度器的
scheduleCallback
,去获取时间分片 - 并发更新调用
performConcurrentWorkOnRoot
,开始进行render
阶段的渲染 - 进入render阶段,调用
renderRoot
,此时第一次渲染wipRootRenderLane为Nolane
, 此时的任务更新的lane
为4
。 所以执行prepareFreshStack
从根节点开始调度。
if (wipRootRenderLane !== lane) {
prepareFreshStack(root, lane);
}
- 由于本次是并发更新,接下来调用
workLoopConcurrent
。 - 在
workLoopConcurrent
中,当时间分片用完后,unstable_shouldYield
返回true
,应该中断,将控制权还给浏览器
到这一步,第一次浏览器和react之间的交互已经完成。接下来等待浏览器下一次空闲时间,再次调度。
// 中断
if (exitStatus === RootInComplete) {
// ensureRootIsScheduled中有更高的优先级插入进来, 停止之前的调度
if (root.callbackNode !== curCallbackNode) {
return null;
}
// 继续调度
return performConcurrentWorkOnRoot.bind(null, root);
}
在中断的过程中,我们返回的函数performConcurrentWorkOnRoot.bind(null, root);
。scheduler
调度器会在下次空闲的时候,再次调用performConcurrentWorkOnRoot
再次调度:
performConcurrentWorkOnRoot
执行, 再次获取优先级,此时优先级没有变化,还是返回上一次被中断的任务的优先级- 继续调用render阶段的
renderRoot
。 - 进入
renderRoot
后发现wipRootRenderLane === lane
优先级没有变化,就不会调用prepareFreshStack
再次从头开始调度。而是继续上一次的调度点继续调度。这样就可以将一个大的任务,切分成很多小的任务。而不堵塞浏览器的渲染。 - 接下来继续等待
unstable_shouldYield
返回true
。中断本次的workLoopConcurrent
。
重复这个流程。直到任务完成或者有更高优先级的任务进入,打断了本次更新的任务。
例子 - 高优先级任务打断正在执行低优先级任务
例如,当我们在useEffect
中更新视图的时候,突然来了一个更高优先级的任务。这个时候就出现了高优先级打断低优先级的情况。
- 当一个低优先级的任务正在渲染过程中。一个高优先级的任务突然被插入
scheduleUpdateOnFiber
被执行,其他的流程都任务进行中的步骤一样。主要的区别是要取消之前的更新,以及从头开始遍历- 在
ensureRootIsScheduled
中取消上一个低优先级任务// 当前产生了更高优先级调度,取消之前的调度 if (existingCallback !== null) { unstable_cancelCallback(existingCallback); }
- 由于优先级现在不同,在
renderRoot
中就需要从头开始调度if (wipRootRenderLane !== lane) { // 初始化,将workInProgress 指向第一个fiberNode prepareFreshStack(root, lane); }
转载自:https://juejin.cn/post/7212101192745500728