本文主要是介绍【React原理 - 任务调度之中断恢复】,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
概览
本文紧接上文介绍React调度的时间分片中任务中断和恢复,由于篇幅过长,所以拆成了两篇。上文主要介绍了调度器中的优先级和调度任务的触发、注册和调度循环。本文主要从任务调度入手介绍调度任务之后发送了什么,即在协调器中如何进行到fiber构造的一系列流程。所以推荐在阅读本文之前先阅读上文,对调度有个基本认识。
前情回顾
先看下图了解React中宏观上整体执行流程图:
React有两大循环:Scheduler workLoop(调度循环)、fiber workLoop(fiber构造循环)。在上文中介绍了调度循环,其workLoop代码如下:
// packages/scheduler/src/forks/Scheduler.js
function workLoop(hasTimeRemaining, initialTime) {let currentTime = initialTime;advanceTimers(currentTime);currentTask = peek(taskQueue);while (currentTask !== null &&!(enableSchedulerDebugging && isSchedulerPaused)) {if (currentTask.expirationTime > currentTime &&(!hasTimeRemaining || shouldYieldToHost())) {// This currentTask hasn't expired, and we've reached the deadline.break;}const callback = currentTask.callback;if (typeof callback === 'function') {currentTask.callback = null;currentPriorityLevel = currentTask.priorityLevel;const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;const continuationCallback = callback(didUserCallbackTimeout);currentTime = getCurrentTime();if (typeof continuationCallback === 'function') {currentTask.callback = continuationCallback;} else {if (currentTask === peek(taskQueue)) {pop(taskQueue);}}advanceTimers(currentTime);} else {pop(taskQueue);}currentTask = peek(taskQueue);}// Return whether there's additional workif (currentTask !== null) {return true;} else {const firstTimer = peek(timerQueue);if (firstTimer !== null) {requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);}return false;}
}
该函数具体逻辑在上文已经介绍,所以本文不再细说,主要看while循环中执行callback回调,即const continuationCallback = callback(didUserCallbackTimeout);
,这里的callback就是在Reconciler通过scheduleCallback传入的performConcurrentWorkOnRoot
函数,所以每次调度执行的就是该函数,来进行fiber的构造。
performConcurrentWorkOnRoot
主要逻辑如下:
// root: 根fiber节点
// didTimeout:当前调度任务是否过期: currentTask.expirationTime <= currentTime
function performConcurrentWorkOnRoot(root, didTimeout) {// 当前任务没有过期并且不是阻塞任务,而且任务没有过期的时候开启分片,否则就进行同步更新,也避免了频繁被高优先级中断而导致低优先级任务无法执行的问题const shouldTimeSlice =!includesBlockingLane(root, lanes) &&!includesExpiredLane(root, lanes) &&(disableSchedulerTimeoutInWorkLoop || !didTimeout);let exitStatus = shouldTimeSlice? renderRootConcurrent(root, lanes): renderRootSync(root, lanes);if (exitStatus !== RootInProgress) {// The render completed.const finishedWork: Fiber = (root.current.alternate: any);root.finishedWork = finishedWork;root.finishedLanes = lanes;finishConcurrentRender(root, exitStatus, lanes); // commitRoot}ensureRootIsScheduled(root, now()); // 调度ensureRootIsScheduled开启下一次调度,如果有中断的任务,则再次等待调度if (root.callbackNode === originalCallbackNode) {return performConcurrentWorkOnRoot.bind(null, root); // 保存当前的上下文,如果任务被中断就根据该上下文进行恢复,即调度循环中的continuationCallback}return null;
}
从代码能看出来并不是所有的并发任务都能分片的,只有当前任务没有过期、不是阻塞性任务(比如用户交互事件:onclick、input…)、以及没有超时的任务,才会开启分片执行renderRootConcurrent
函数进行并发渲染。
由于disableSchedulerTimeoutInWorkLoop表示是否禁用超时检查,并且该值默认是false。所以每次执行前都会检查didTimeout当前任务是否超时,如果超时则进行同步处理不可中断。以此来避免低优先级任务一直被高优先级任务中断而导致始终无法执行的问题。
如上可知,根据是否开启分片来判断是同步(renderRootSync)还是异步执行(renderRootConcurrent)。本文主要介绍并发中断和恢复,所以主要介绍异步执行即renderRootConcurrent
函数的逻辑。
renderRootConcurrent
函数如下:
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {const prevExecutionContext = executionContext;executionContext |= RenderContext;if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {prepareFreshStack(root, lanes); // 全局状态变量初始化 包括workInProgressRootRenderLanes等}do {try {workLoopConcurrent();break;} catch (thrownValue) {handleError(root, thrownValue);}} while (true);executionContext = prevExecutionContext;// Check if the tree has completed.if (workInProgress !== null) {return RootInProgress;} else {// Set this to null to indicate there's no in-progress render.workInProgressRoot = null;workInProgressRootRenderLanes = NoLanes;// Return the final exit status.return workInProgressRootExitStatus;}}
在代码中提到了栈帧即prepareFreshStack
当更新的root或者优先级变化时,则认为是一次全新的调度,所以会调用prepareFreshStack
初始化一个栈帧。该栈帧主要在内存中用全局变量保存当前fiber构造的状态,以便被高优先级任务中断之后能通过该组全局变量来恢复中断是的状态
。因为在被高优先级任务中断时会将当前状态保存在全局变量中,包括上面的workInProgressRootRenderLanes
,然后在高优先级任务执行之后会通过该优先级判断,根据全局变量获取中断时的内存状态,并恢复执行。并且workInProgress 指针指向这些全局变量, 即 workInProgress = HostRootFiber.alternate
。
初始化栈帧之后会死循环调用workLoopConcurrent
来进行fiber构造:
function workLoopConcurrent() {// Perform work until Scheduler asks us to yieldwhile (workInProgress !== null && !shouldYield()) {performUnitOfWork(workInProgress);}}
通过while一直调用performUnitOfWork
来进行beginWork、completeWork构造fiber节点。具体fiber构造流程可查看这篇文章:【React架构 - Fiber构造循环】
当workLoopConcurrent
执行完成获取被中断而跳出死循环后,会根据workInProgress来判断当然任务是否完成进而返回RootInProgress / workInProgressRootExitStatus
。然后在performConcurrentWorkOnRoot
函数中exitStatus
来接收该返回值。当执行完成之后会设置完成状态finishedWork、finishedLanes
并通过finishConcurrentRender
来触发commit将创建的新fiber树提交到commit阶段进行真实dom的操作。
然后会调用ensureRootIsScheduled
来再发出一次调度(MessageChannel宏任务),在下一次事件循环时执行,并保存当前的上下文,如果任务被中断就根据该上下文进行恢复,即调度循环中的continuationCallback
。
当callback即performConcurrentWorkOnRoot
执行完之后会通过continuationCallback
接收返回值,即上面提到的performConcurrentWorkOnRoot(未执行完时会返回performConcurrentWorkOnRoot函数并传入保存有中断时的状态变量的root
)/ null(执行完成)。然后会判断该返回值,如果未完成则会将performConcurrentWorkOnRoot
绑定到当前任务的callback,该任务仍然在taskQueue中,下一次任务调度时继续从taskQueue中取出执行,执行完成之后则将调度任务从taskQueue中移出。
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();if (typeof continuationCallback === 'function') {currentTask.callback = continuationCallback;} else {if (currentTask === peek(taskQueue)) {pop(taskQueue);}}advanceTimers(currentTime);
总结
在React中主要是通过shouldYieldToHost
来进行时间分片(5ms)或者高优先级任务中断,然后当前任务中断时会将此时的状态保存在全局变量并绑定在workInProgress中(栈帧
),并返回performConcurrentWorkOnRoot
(传入保存了此时状态的root即workInProgress)作为调度任务的callback,继续保存taskQueue中。然后调用ensureRootIsScheduled
再发起一起调度任务,待高优先级任务执行完成之后,新一次调度继续从taskQueue中取出该任务,并利用workInProgress保持的状态恢复中断前进度,并继续执行。
- 使用shouldYieldToHost时间分片、高优先级中断
- 利用workInProgress保存状态恢复
- 未完成时更新调度任务的callback继续保留在taskQueue中,下一次调度获取
具体详细流程可以查看该图:(图片来源来自网上)
这篇关于【React原理 - 任务调度之中断恢复】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!