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

news2024/9/20 0:32:14

概览

本文紧接上文介绍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 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主要逻辑如下:

// 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

栈帧

调度任务的中断和恢复主要通过栈帧(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构造:

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中移出。

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进行中断,利用栈帧进行恢复。具体详细流程可以查看该图:(图片来源来自网上)
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2095048.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【matlab】数组操作:寻找最大值和最小值及其位置ind2sub函数

【matlab】数组操作&#xff1a;寻找最大值和最小值及其位置ind2sub函数 本文将介绍如何在MATLAB环境中使用内置函数来创建数组&#xff0c;以及如何找到数组中的最大值和最小值及其对应的位置。通过示例代码&#xff0c;我们将一步步展示这一过程&#xff0c;帮助读者更好地理…

探索Python的测试之道:unittest库的奥秘

文章目录 探索Python的测试之道&#xff1a;unittest库的奥秘背景&#xff1a;为何选择unittest&#xff1f;什么是unittest库&#xff1f;如何安装unittest库&#xff1f;简单库函数使用方法场景应用场景一&#xff1a;测试数学运算场景二&#xff1a;测试异常处理场景三&…

armv8 memory model概述

概述 在armv8 架构中&#xff0c;它引入了更多的维度来描述内存模型&#xff0c;从而在此基础上进行硬件优化(但其中一些并未被主流的软件所接受)&#xff0c;在此做一些简单的整理&#xff0c;更多信息请参考 Arm spec 以及 AMBA 协议。下文主要是对Memory 和 Device 两大类的…

Python 算法交易实验86 QTV200日常推进-获取A股日交易额并统计

说明 上一篇说到&#xff0c;交易量可能可以作为策略规则的支持度分析&#xff0c;但是(我现在还不想付费买数据)现成的接口似乎并没有这样的统计。获取某一只股票的日交易数据是相对简单的&#xff0c;市场上也就不到5000只的股票&#xff0c;总数据量应该也不会超过18M(5000…

面向整个价值链的高可信度卫星测试解决方案

在动态行业格局中增强卫星任务能力 在罗德与施瓦茨&#xff0c;我们利用专业知识和量身定制的测试和测量解决方案为卫星行业提供支持。这包括帮助行业参与者满足完整测试路径的要求&#xff0c;以实现完美的系统性能&#xff0c;确保符合最新技术和标准。此外&#xff0c;我们…

2024年8月文章一览

2024年8月编程人总共更新了3篇文章&#xff1a; 1.2024年7月文章一览 2.《Programming from the Ground Up》阅读笔记&#xff1a;p95-p102 3.《Programming from the Ground Up》阅读笔记&#xff1a;p103-p116 8月&#xff0c;对自己而言是糟糕的一个月&#xff0c;两个项…

4-8 进入C语言,并跳转到loarder(1)

1 总体的逻辑。 从这里开始 写 loader 代码了。 1 首先是 从汇编跳转到 C原因呢&#xff0c; 2 然后是 &#xff0c; 再源码中新建新的loader 目录。 3 一直跳转到 loader 目录的C语言函数里面&#xff0c; 做循环操作。 2 代码&#xff1a; 首先 是从 start.s 跳转到 boot…

论文阅读:MicroNet: Towards Image Recognition with Extremely Low FLOPs

论文地址&#xff1a;https://arxiv.org/pdf/2011.12289 发表时间&#xff1a;2022 项目地址&#xff1a;https://github.com/liyunsheng13/micronet MicroNet是一种有效的卷积神经网络&#xff0c;使用极低的计算成本&#xff08;例如&#xff0c;在ImageNet分类上的6M FLOPs…

基于STM32的无线语音放大系统设计

本设计基于STM32设计了一种无线语音放大系统。该系统由语音采集模块、STM32核心控制模块、NRF24L01无线通信模块和语音放大模块组成。语音采集模块承担着对采集到的语音信号进行预处理的任务。STM32单片机负责控制整个系统的运行过程&#xff0c;包括数据处理、发送端的模数转换…

华为云征文|华为云Flexus X实例ultralytics模型yolov10深度学习AI部署与应用

目录 前言&#xff1a; 环境准备 购买服务器配置 连接服务器 安装Python 安装Pytorch 部署YOLOv10 拉取YOLOv10代码并安装相关依赖 数据集准备 Detect目标检测模型训练 训练数据集的配置文件 训练命令 识别命令 前言&#xff1a; 本实验演示从0到1部署YOLOv10深度…

tecplot宏批量导入数据

Tecplot新手进阶——使用tecplot宏操作批量处理数据输出图片&#xff08;详细步骤&#xff09; tecplot 宏的使用方法及代码改写 第一步&#xff1a;首先点击Scripting–>Record Macro&#xff0c;生成一个脚本文件&#xff0c;即.mcr文件 点击保存&#xff0c;会出现这个…

Activity的优雅跳转

需求1&#xff1a;跳转到下一个界面&#xff1a;NextActivity startActivity(Intent(this,NextActivity::class.java))需求2&#xff1a;跳转到下一个界面(并传参数)&#xff1a;NextActivity startActivity(Intent(this,NextActivity::class.java).apply { putExtra("pa…

Vulnhub靶场 | DC系列 - DC9

文章目录 DC-9环境搭建渗透测试端口敲门服务 DC-9 环境搭建 靶机镜像下载地址&#xff1a;https://vulnhub.com/entry/dc-6,315/需要将靶机和 kali 攻击机放在同一个局域网里&#xff1b;本实验kali 的 IP 地址&#xff1a;192.168.10.146。 渗透测试 使用 nmap 扫描 192.1…

【Linux】Ubuntu 安装 NFS 步骤详解

Ubuntu 安装 NFS 步骤详解 1.安装 NFS 服务2.配置 NFS 服务3.启用 NFS 服务4.客户端挂载 NFS 共享目录5.验证挂载是否成功6.持久化挂载&#xff08;可选&#xff09;7.其他客户端节点安装 NFS&#xff08;Network File System&#xff09;是一种允许不同主机之间通过网络共享文…

快速了解Git 文件的四种状态及其操作指令、如何忽略文件

&#x1f600;前言 在软件开发过程中&#xff0c;版本控制是团队协作的基石之一。Git 作为分布式版本控制系统&#xff0c;被广泛应用于各类项目的管理中。在使用 Git 进行文件管理时&#xff0c;理解文件的不同状态以及如何有效操作这些文件是非常重要的。本文将介绍 Git 文件…

BaseCTF之web(week2)

目录 ez_ser 一起吃豆豆 你听不到我的声音 Really EZ POP RCEisamazingwithspace 所以你说你懂 MD5? 数学大师 ez_ser <?php highlight_file(__FILE__); error_reporting(0);class re{public $chu0;public function __toString(){if(!isset($this->chu0)){return…

Docker 容器编排之 Docker Compose

目录 1 Docker Compose 概述 1.1 主要功能 1.2 工作原理 1.3 Docker Compose 中的管理层 2 Docker Compose 的常用命令参数 2.1 服务管理 2.1.1 docker-compose up &#xff1a; 2.1.2 docker-compose down &#xff1a; 2.1.3 docker-compose start &#xff1a; 2.1.4 docker…

数据结构:(LeetCode101)对称二叉树

给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true示例 2&#xff1a; 输入&#xff1a;root [1,2,2,null,3,null,3] 输出&#xff1a;false提示&#xff1a; 树中节点数目在范围…

World of Warcraft [CLASSIC][80][Shushia] [Obsidian Sanctum][Sartharion]

黑曜石圣殿 [Obsidian Sanctum] 萨塔里奥[Sartharion] 号旗披风、龙魂图典、五色巨龙之怒、黑曜石巨盔等装备&#xff0c;都是非常极品的BIS装备 召唤顺序&#xff1a;&#xff08;中&#xff09;塔尼布隆、&#xff08;右&#xff09;沙德隆、&#xff08;左&#xff09;维斯…