【React原理 - 任务调度之中断恢复】

概览

本文紧接上文介绍React调度的时间分片中任务中断和恢复,由于篇幅过长,所以拆成了两篇。上文主要介绍了调度器中的优先级和调度任务的触发、注册和调度循环。本文主要从任务调度入手介绍调度任务之后发送了什么,即在协调器中如何进行到fiber构造的一系列流程。所以推荐在阅读本文之前先阅读上文,对调度有个基本认识。

前情回顾

先看下图了解React中宏观上整体执行流程图:

React有两大循环:Scheduler workLoop(调度循环)、fiber workLoop(fiber构造循环)。在上文中介绍了调度循环,其workLoop代码如下:

javascript 复制代码
// 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 work
  if (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主要逻辑如下:

javascript 复制代码
// 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函数如下:

javascript 复制代码
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

栈帧

调度任务的中断和恢复主要通过栈帧(Stack Frame)和 Fiber 树状态的保存与恢复来实现。这些栈帧是由 React 内部的机制来管理的,确保任务在中断和恢复之间能保持正确的执行上下文。以下是关于栈帧在任务中断和恢复过程中的具体作用和机制

栈帧的作用

1、初始化栈帧 :在执行任务时,React 会为当前的 Fiber 树准备一个新的栈帧(通过 prepareFreshStack)。这个栈帧包含了当前任务的上下文,包括当前 Fiber 节点的状态、渲染优先级等信息。

2、保存任务状态:当任务执行时,React 会不断更新栈帧中的状态。栈帧会记录 Fiber 树的当前执行点,以及当前任务的执行上下文。这个状态在执行过程中被不断更新,以便在任务中断时能保存当前的执行进度。

3、中断时刷新栈帧 :当高优先级任务需要执行时,当前的低优先级任务会被中断。此时,React 会将当前栈帧中的信息保存到内存中(包括 workInProgress、workInProgressRoot、workInProgressRootRenderLanes 等)。中断时栈帧会被刷新或重置,以便开始处理高优先级的任务。

中断后的栈帧刷新与状态恢复

1、刷新栈帧:当一个任务被中断,并且需要执行一个新任务(例如高优先级任务)时,React 会刷新当前的栈帧。这意味着 React 会重置栈帧,以准备处理新的任务上下文。

2、恢复中断任务时的状态

  • 在高优先级任务执行完毕之后,React 会恢复之前中断的任务。恢复的关键在于从之前保存的 Fiber 树状态中读取并重建栈帧。
  • 保留的状态:中断时保存的 workInProgress Fiber 节点等信息存储了当前 Fiber 树的执行状态。这些信息并不会被高优先级任务的执行所覆盖。
  • 恢复机制:React 通过保存的 workInProgress 等信息重新设置执行点,将中断时的上下文重新加载到栈帧中,以便继续从中断点执行。

3、继续执行:恢复状态后,React 从中断点继续执行 performUnitOfWork,完成剩余的任务。栈帧中的状态会被逐步更新,直到任务完成。

栈帧小结

  • 栈帧刷新:在执行高优先级任务时,会刷新栈帧,以便新的任务能够正确执行。
  • 状态恢复:中断任务的状态不会被高优先级任务的栈帧所覆盖。当中断任务恢复时,React 会从之前保存的内存状态中读取,并重新初始化栈帧,使任务能继续执行。

初始化栈帧之后会死循环调用workLoopConcurrent来进行fiber构造:

javascript 复制代码
function workLoopConcurrent() {
    // Perform work until Scheduler asks us to yield
    while (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中移出。

javascript 复制代码
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中(传入的root节点),并返回performConcurrentWorkOnRoot(传入保存了此时状态的root即workInProgress)作为调度任务的callback,继续保存taskQueue中。然后调用ensureRootIsScheduled再发起一起调度任务,待高优先级任务执行完成之后,新一次调度继续从taskQueue中取出该任务,并利用workInProgress保存的状态恢复中断前进度,并继续执行。

  • 使用shouldYieldToHost时间分片、高优先级中断
  • 利用栈帧保存状态绑定在workInProgress上恢复
  • 未完成时更新调度任务的callback继续保留在taskQueue中,下一次调度获取

简而言之,调度的中断恢复就是一句话:使用shouldYieldToHost进行中断,利用栈帧进行恢复。具体详细流程可以查看该图:(图片来源来自网上)

相关推荐
酷酷的阿云9 分钟前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:1379712058712 分钟前
web端手机录音
前端
齐 飞17 分钟前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
神仙别闹34 分钟前
基于tensorflow和flask的本地图片库web图片搜索引擎
前端·flask·tensorflow
GIS程序媛—椰子1 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_0012 小时前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端2 小时前
Content Security Policy (CSP)
前端·javascript·面试
木舟10092 小时前
ffmpeg重复回听音频流,时长叠加问题
前端
王大锤43912 小时前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang
我血条子呢2 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js