【React源码 - 调度任务循环EventLoop】

我们知道在React中有4个核心包、2个关键循环。而React正是在这4个核心包中运行,从输入到输出渲染到web端,主要流程可简单分为一下4步:如下图,本文主要是介绍两大循环中的任务调度循环。
在这里插入图片描述

4个核心包
react: 基础包
react-dom:渲染器,连接react和web,通常使用ReactDOM.render(, document.getElementById(‘root’))来挂载组件到指定dom,是入口文件
react-scheduler:调度器,独立的包,主要是任务优先级调度(时间分片、支持可中断渲染)
react-reconciler: 协调器,综合协调react-dom,react,scheduler各包之间的调用与配合),将输入信号转换为输出信号给到渲染器,就是将状态的更新,构建新的fiber树给到react-dom进行渲染到web

2个关键循环
任务调度循环(Event Loop)在Scheduler中实现
fiber构造循环,在Reconciler中实现
其中任务调度循环包含fiber构造、dom渲染、调度检测,fiber构造只是其子集

入口

由上面的图可以看出,从react-dom开始,一旦发生状态更新等输入就会依次触发各个回调进行处理,关键流程如下:schedulerUpdateOnFiber ->ensureRootIsScheduled -> scheduleSyncCallback/scheduleCallback(同步/异步) -> Scheduler(进入react-scheduler中进行任务调度)
在这里插入图片描述

重要源码解析

scheduleUpdateOnFiber:两种结果
1、不经过调度, 直接进行fiber构造.
2、注册调度任务, 经过Scheduler包的调度, 间接进行fiber构造.

// 唯一接收输入信号的函数
export function scheduleUpdateOnFiber(
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  // ... 省略部分无关代码
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  if (lane === SyncLane) {
    if (
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      // 直接进行`fiber构造`
      performSyncWorkOnRoot(root);
    } else {
      // 注册调度任务, 经过`Scheduler`包的调度, 间接进行`fiber构造`
      ensureRootIsScheduled(root, eventTime);
    }
  } else {
    // 注册调度任务, 经过`Scheduler`包的调度, 间接进行`fiber构造`
    ensureRootIsScheduled(root, eventTime);
  }
}

ensureRootIsScheduled: 分为 2 部分:
1、前半部分: 判断是否需要注册新的调度(如果无需新的调度, 会退出函数)
2、后半部分: 注册调度任务performSyncWorkOnRoot或

  • performConcurrentWorkOnRoot被封装到了任务回调(scheduleSyncCallback或scheduleCallback)中
  • 等待调度中心执行任务, 任务运行其实就是执行performSyncWorkOnRoot或performConcurrentWorkOnRoot
// ... 省略部分无关代码
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  // 前半部分: 判断是否需要注册新的调度
  const existingCallbackNode = root.callbackNode;
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );
  const newCallbackPriority = returnNextLanesPriority();
  if (nextLanes === NoLanes) {
    return;
  }
  if (existingCallbackNode !== null) {
    const existingCallbackPriority = root.callbackPriority;
    if (existingCallbackPriority === newCallbackPriority) {
      return;
    }
    cancelCallback(existingCallbackNode);
  }

  // 后半部分: 注册调度任务
  let newCallbackNode;
  if (newCallbackPriority === SyncLanePriority) {
    newCallbackNode = scheduleSyncCallback(
      performSyncWorkOnRoot.bind(null, root),
    );
  } else if (newCallbackPriority === SyncBatchedLanePriority) {
    newCallbackNode = scheduleCallback(
      ImmediateSchedulerPriority,
      performSyncWorkOnRoot.bind(null, root),
    );
  } else {
    const schedulerPriorityLevel =
      lanePriorityToSchedulerPriority(newCallbackPriority);
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

至此,我们正式进入到Scheduler中来介绍任务调度循环中是如何创建任务并处理时间分片以及至此可终端渲染的。

Scheduler任务调度

从下面的示意图能看出,在Scheduler中,通过unstable_scheduleCallback来触发创建任务(下面简称task),创建完成之后添加到任务队列(taskQueue)然后调用requestHostCallback来请求调用,通过MessageChannel(EvenLoop)进入任务调度循环等待调用,调用之后会将包含任务的callback传回到Reconciler中调用,执行performSyncWorkOnRoot/performConcurrentWorkOnRoot(异步/同步)进行到fiber构造循环
在这里插入图片描述

创建调度任务

通过unstable_scheduleCallback来创建新的任务,主要是根据任务优先级来设置任务过期时间(优先级越高,值越小,过期时间越短,在队列中排序越靠前sortIndex),然后将生成的newTask加入taskQueue,并请求调用,处于等待调用状态。

// 省略部分无关代码
function unstable_scheduleCallback(priorityLevel, callback, options) {
  // 1. 获取当前时间
  var currentTime = getCurrentTime();
  var startTime;
  if (typeof options === 'object' && options !== null) {
    // 从函数调用关系来看, 在v17.0.2中,所有调用 unstable_scheduleCallback 都未传入options
    // 所以省略延时任务相关的代码
  } else {
    startTime = currentTime;
  }
  // 2. 根据传入的优先级, 设置任务的过期时间 expirationTime
  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = IMMEDIATE_PRIORITY_TIMEOUT;
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  }
  var expirationTime = startTime + timeout;
  // 3. 创建新任务
  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  if (startTime > currentTime) {
    // 省略无关代码 v17.0.2中不会使用
  } else {
    newTask.sortIndex = expirationTime;
    // 4. 加入任务队列
    push(taskQueue, newTask);
    // 5. 请求调度
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }
  return newTask;
}

任务对象结构:

var newTask = {
  id: taskIdCounter++, // id: 一个自增编号
  callback, // callback: 传入的回调函数
  priorityLevel, // priorityLevel: 优先级等级
  startTime, // startTime: 创建task时的当前时间
  expirationTime, // expirationTime: task的过期时间, 优先级越高 expirationTime = startTime + timeout 越小
  sortIndex: -1,
};
newTask.sortIndex = expirationTime; // sortIndex: 排序索引, 全等于过期时间. 保证过期时间越小, 越紧急的任务排在最前面

Scheduler优先级

由于创建task中提及到优先级,所以在这里也简单介绍一下,在React中主要有三种优先级:

  • fiber优先级(LanePriority): 位于react-reconciler包, 也就是Lane(车道模型).
  • 调度优先级(SchedulerPriority): 位于scheduler包.
  • 优先级等级(ReactPriorityLevel) : 位于react-reconciler包中的SchedulerWithReactIntegration.js, 负责上述 2 套优先级体系的转换.
    简单理解就是LanePriority是react-reconciler里面的优先级等级、SchedulerPriority是Scheduler中的优先级等级,两者没有直接联系,是通过彼此和ReactPriorityLevel相互转换,产生间接联系。

优先级等级是由二进制进行表示,值越小等级越高,通过 lane & -lane来获取等级最大值

32位二进制,最高位表示符号位,所以表示值的只有31位

消费任务

由上面可知,创建task之后就会调用requestHostCallback(flushWork)来发起请求调用到调度中心,并等待调用,其中flushWork回调中就是处理workLoop来消费队列的回调,当调度中心调度flushWork时,就会调用workLoop来循环消费任务队列中的队列,即worlLoop中就是消费taskQueue的回调。

flushWork中就是设置全局标志,并调用workLoop

function flushWork(hasTimeRemaining, initialTime) {
  // 1. 做好全局标记, 表示现在已经进入调度阶段
  isHostCallbackScheduled = false;
  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
    // 2. 循环消费队列
    return workLoop(hasTimeRemaining, initialTime);
  } finally {
    // 3. 还原全局标记
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
  }
}

在workLoop中处理消费taskQueue中的任务,其中进行了时间分片和可中断的处理:

// 省略部分无关代码
function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime; // 保存当前时间, 用于判断任务是否过期
  currentTask = peek(taskQueue); // 获取队列中的第一个任务
  while (currentTask !== null) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // 虽然currentTask没有过期, 但是执行时间超过了限制(毕竟只有5ms, shouldYieldToHost()返回true). 停止继续执行, 让出主线程
      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') {
        // 产生了连续回调(如fiber树太大, 出现了中断渲染), 保留currentTask
        currentTask.callback = continuationCallback;
      } else {
        // 把currentTask移出队列
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
    } else {
      // 如果任务被取消(这时currentTask.callback = null), 将其移出队列
      pop(taskQueue);
    }
    // 更新currentTask
    currentTask = peek(taskQueue);
  }
  if (currentTask !== null) {
    return true; // 如果task队列没有清空, 返回true. 等待调度中心下一次回调
  } else {
    return false; // task队列已经清空, 返回false.
  }
}

在workLoop中会循环taskQueue中的task,每次取出第一个task,然后以task为单位执行,会先判断任务的过期时间(expirationTime)以及是否要移交主线程(shouldYieldToHost),满足条件之后才会处理该task。然后设置task的callback为null(很关键,下面会根据callback来判断这个task是否执行完,保存中断时的task的快照),执行callback,如果这期间产生了中断,则会返回continuationCallback回调会保存currentTask否在会从队列中删除该task,表示该task以及执行完成。workLoop循环消费taskQueue的示意图如下:
在这里插入图片描述

shouldYieldToHost判断是否需要将主流程让给其他任务使用,因为Js是单线程,比如在准备消费task之前有用户IO操作或者当前taskQueue中task较多,占用时间太长(时间分片周期为5ms)就需要让出主线程,等待下一次调度中心的调度,shouldYieldToHost源码下面会介绍。

回到主线,刚说到创建完成之后通过把处理taskQueue的flushWork回调传给requestHostCallback来申请调度。调度示意图如下:
在这里插入图片描述
下面我们从代码来看看这个函数中做了什么:

// 请求回调
requestHostCallback = function (callback) {
  // 1. 保存callback
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    // 2. 通过 MessageChannel 发送消息
    port.postMessage(null);
  }
};

在里面通过MessageChannel来发布了一个消息,然后会有performWorkUntilDeadline来接收到该消息

为什么使用messageChannel来进行调度和时间分片,不使用settimeout或浏览器提供的api: requestAnimationFrame、requestIdleCallback呢? 请查看写的这篇文章:【React架构 - Scheduler中的MessageChannel】

// 接收 MessageChannel 消息
const performWorkUntilDeadline = () => {
  // ...省略无关代码
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    // 更新deadline
    deadline = currentTime + yieldInterval;
    // 执行callback
    scheduledHostCallback(hasTimeRemaining, currentTime);
  } else {
    isMessageLoopRunning = false;
  }
};

从代码里面可以看到,在performWorkUntilDeadline接收到requestHostCallback发送的消息后更新deadline之后就调用了scheduledHostCallback来执行该任务,这里的scheduledHostCallback就是刚才传入的flushWork,来循环处理消费taskQueue。在workLoop中每次消费task之前都会判断shouldYieldToHost,下面来介绍一下该函数主要做了什么

// 获取当前时间
getCurrentTime = () => localPerformance.now();

// 时间切片周期, 默认是5ms(如果一个task运行超过该周期, 下一个task执行之前, 会把控制权归还浏览器)
let yieldInterval = 5;
let deadline = 0;
const maxYieldInterval = 300;
let needsPaint = false;
const scheduling = navigator.scheduling;
// 是否让出主线程
shouldYieldToHost = function () {
  const currentTime = getCurrentTime();
  if (currentTime >= deadline) {
    if (needsPaint || scheduling.isInputPending()) {
      // There is either a pending paint or a pending input.
      return true;
    }
    // There's no pending input. Only yield if we've reached the max
    // yield interval.
    return currentTime >= maxYieldInterval; // 在持续运行的react应用中, currentTime肯定大于300ms, 这个判断只在初始化过程中才有可能返回false
  } else {
    // There's still time left in the frame.
    return false;
  }
};

从代码中能看出来shouldYieldToHost就是判断当前task是否过期以及是否需要马上绘制或者有IO操作。时间分片周期默认为5ms,最大为300ms,返回为true,就需要将控制器教换给浏览器立即退出任务调度循环,每次循环都会判断一次入上面workLoop所见。时间分片周期默认是5ms,当然也可以根据不同设备的fps来进行设定:

// 设置时间切片的周期
forceFrameRate = function (fps) {
  if (fps < 0 || fps > 125) {
    // Using console['error'] to evade Babel and ESLint
    console['error'](
      'forceFrameRate takes a positive int between 0 and 125, ' +
        'forcing frame rates higher than 125 fps is not supported',
    );
    return;
  }
  if (fps > 0) {
    yieldInterval = Math.floor(1000 / fps);
  } else {
    // reset the framerate
    yieldInterval = 5;
  }
};

至此EventLoop中主要的流程已经介绍完了,随后便是将消费task将callback传入到Reconciler中执行performSyncWorkOnRoot/performConcurrentWorkOnRoot来进行Fiber构造,进入React两大循环中的fiber构造循环了。

参考资料

图解React

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL 篇-深入了解 DML、DQL 语言(二)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 DML、DQL 语言说明 2.0 使用 DML 实现对数据管理和操作 2.1 DML - 增添数据 insert 2.2 DML - 修改数据 update 2.3 DML - 删除数据 delete 3.0 使用 DQL 实现对…

使用.NET 升级助手工具将.net framework4.8 MVC项目升级到net6

1 新建.net framework4.8 MVC项目 随便添加一个可以访问的界面用于测试 2 对当前项目进行升级 注意&#xff1a;若要进行升级&#xff0c;首先确保本地已安装相应的sdk&#xff0c;例如&#xff1a;dotnet-sdk-6.0.402-win-x64.exe1.运行cmd命令窗口&#xff0c;进入项目所在…

【蓝桥杯】快读|min和max值的设置|小明和完美序列|​顺子日期​|星期计算|山

目录 一、输入的三种方式 1.最常见的Scanner的输入方法 2.数据多的时候常用BufferedReader快读 3.较麻烦的StreamTokenizer快读&#xff08;用的不多&#xff09; StreamTokenizer常见错误&#xff1a; 二、min和max值的设置 三、妮妮的翻转游戏 四、小明和完美序列 五…

Docker之数据卷自定义镜像

目录 一、数据卷 ​二、自定义镜像 2.1自定义centos 一、数据卷 在docker中&#xff0c;数据卷是宿主机的一个可以供一个或多个容器使用的特殊目录&#xff0c;它可以在容器之间共享和重用&#xff0c;本地与容器间传递数据更高效&#xff1b;对数据卷的修改会立马有效&#…

CSS——PostCSS简介

文章目录 PostCSS是什么postCSS的优点补充&#xff1a;polyfill补充&#xff1a;Stylelint PostCSS架构概述工作流程PostCSS解析方法PostCSS解析流程 PostCSS插件插件的使用控制类插件包类插件未来的CSS语法相关插件后备措施相关插件语言扩展相关插件颜色相关组件图片和字体相关…

代码库管理工具Git介绍

阅读本文同时请参阅-----免费的Git图形界面工具sourceTree介绍 Git是一个分布式版本控制系统&#xff0c;它可以帮助开发者跟踪和管理代码历史。Git的命令行工具是使用Git的核心方式&#xff0c;虽然它可能看起来有些复杂&#xff0c;但是一旦掌握了基本命令&#xff0c;你…

基于MQTT协议实现微服务架构事件总线

一、场景描述 昨天在博客《客户端订阅服务端事件的实现方法》中提出了利用websocket、服务端EventEmitter和客户端mitt实现客户端订阅服务端事件&#xff0c;大大简化了客户端对服务端数据实时响应的逻辑。上述方案适用于单服务节点的情形。 对于由服务集群支撑的微服务架构&…

招聘系统架构的设计与实现

在当今竞争激烈的人才市场中&#xff0c;有效的招聘系统对企业吸引、筛选和管理人才至关重要。本文将探讨招聘系统的架构设计与实现&#xff0c;帮助企业构建一个高效、可靠的人才招聘平台。 ## 1. 系统架构设计 ### 1.1 微服务架构 招聘系统通常采用微服务架构&#xff0c;将…

Java JVM虚拟机面试题

Java JVM虚拟机面试题 前言1、ThreadLocal的底层原理和应用&#xff1f;2、Java中的锁池和等待池&#xff1f;3、wait()&#xff0c;yield()&#xff0c;join()&#xff0c;sleep()的区别&#xff1f;4、你们项⽬如何排查JVM问题&#xff1f;5、YGC和FGC发生时间&#xff1f;6、…

11.vue学习笔记(组件生命周期+生命周期应用+动态组件+组件保持存活)

文章目录 1.组件生命周期2.生命周期应用2.1通过ref获取元素DOM结构2.2.模拟网络请求渲染数据 3.动态组件3.1.A&#xff0c;B两个组件 4.组件保持存活&#xff08;销毁期&#xff09; 1.组件生命周期 每个Vue组件实例在创建时都需要经历一系列的初始化步骤&#xff0c;比如设置…

探索AI视频模型的无限可能:OpenAI的Sora引领创新浪潮

文章目录 &#x1f4d1;前言一、技术解析二、应用场景三、未来展望四、伦理与创意五、用户体验与互动&#x1f324;️总结 &#x1f4d1;前言 随着人工智能技术的蓬勃发展&#xff0c;AI视频模型正逐渐成为科技领域的新宠。在这个变革的浪潮中&#xff0c;OpenAI推出的首个AI视…

Spring中 Unsupported class file major version 61 报错

初学Spring时遇到的一个错误&#xff1a;Unsupported class file major version 61 &#xff0c;如图所示&#xff1a; 网上查了一下大概是JDK的版本与Spring的版本不一致导致的错误&#xff1b;刚开始我用的Spring版本是&#xff1a; <dependencies><dependency>…

(全部习题答案)研究生英语读写教程基础级教师用书PDF|| 研究生英语读写教程提高级教师用书PDF

研究生英语读写教程基础级教师用书PDF 研究生英语读写教程提高级教师用书PDF pdf下载&#xff08;完整版下载&#xff09; &#xff08;1&#xff09;研究生英语读写教程基础级教师用书PDF &#xff08;2&#xff09;研究生英语读写教程基提高级教师用书PDF

【Pytorch深度学习开发实践学习】Pytorch实现LeNet神经网络(1)

1.model.py import torch.nn as nn import torch.nn.functional as F引入pytorch的两个模块 关于这两个模块的作用&#xff0c;可以参考下面 Pytorch官方文档 torch.nn包含了构成计算图的基本模块 torch,nn.function包括了计算图中的各种主要函数&#xff0c;包括&#…

linux gdb 调试工具

1.写程序 首先&#xff0c;我们先写出一个 .c 或者.cpp程序 如 然后 gcc -g hello.c -o hello 或者 g -g hello.cpp -o hello &#xff08;-g&#xff09;要加 2. gdb调试 用 gdb &#xff08;可执行程序&#xff0c;如hello&#xff09; 进入之后&#xff0c;有…

Window系统安装USB Redirector结合cpolar实现远程访问本地USB设备

文章目录 前言1. 安装下载软件1.1 内网安装使用USB Redirector1.2 下载安装cpolar内网穿透 2. 完成USB Redirector服务端和客户端映射连接3. 设置固定的公网地址 前言 USB Redirector是一款方便易用的USB设备共享服务应用程序&#xff0c;它提供了共享和访问本地或互联网上的U…

物业智能水电抄表管理系统

物业智能水电抄表管理系统是物业管理行业的关键技术之一&#xff0c;其结合了智能化、远程监控和数据分析等功能&#xff0c;为物业管理公司和业主提供了高效、精准的水电抄表管理解决方案。该系统具有多项优势&#xff0c;能够提升物业管理效率&#xff0c;降低成本&#xff0…

[计算机网络]--MAC/ARP/DNS协议

前言 作者&#xff1a;小蜗牛向前冲 名言&#xff1a;我可以接受失败&#xff0c;但我不能接受放弃 如果觉的博主的文章还不错的话&#xff0c;还请点赞&#xff0c;收藏&#xff0c;关注&#x1f440;支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 目录 一、认识以…

UnityWebGL UGUI中文不显示问题

这是Unity编辑中效果 打包成webgl后的效果&#xff08;中文没有显示出来&#xff09; 解决方法 将Unity默认使用的Arial替换成中文字体。 1.找到电脑字体库&#xff08;win电脑字体库路径&#xff1a;C:\Windows\Fonts &#xff1b;Mac电脑搜索“字体册”&#xff09;。 2.将…

TC3xx SMU、PMIC和Tranceiver的功能安全闭环

目录 1.TLF35584安全状态输出响应对象 1.1 响应ERR 收集到的错误信号 1.2 响应监控功能引发的ROT 1.3 响应看门狗引发的错误 1.4 环境过温引发的错误状态 1.5 为什么设计SSx&#xff1f; 2. 安全状态输出给谁 3.小结 在之前文章里&#xff0c;我们简述了TC3xx SMU如何…