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

news2025/2/24 15:20:02

我们知道在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呢?
// TODO 这个本文不进行介绍,稍后会针对该问题发一篇文章。

// 接收 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所见。至此EventLoop中主要的流程已经介绍完了,随后便是将消费task将callback传入到Reconciler中执行performSyncWorkOnRoot/performConcurrentWorkOnRoot来进行Fiber构造,进入React两大循环中的fiber构造循环了。

参考资料

图解React

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

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

相关文章

数据结构 队列

一定义 1.1概述&#xff1a; 队列只允许在一端进行插入操作&#xff0c;而在另一端进行删除操作的线性表 特点&#xff1a;队列是先进先出的线性表 允许插入的一端称为队尾&#xff0c;允许删除的一端是队头 这里我们就介绍链式的 1.2 建立队列 这里说一句 其实不管是栈还…

php基础学习之错误处理(其一)

一&#xff0c;错误处理的概念 错误处理指的是系统(或者用户)在执行某些代码的时候&#xff0c;发现有错误&#xff0c;就会通过错误处理的形式告知程序员&#xff0c;俗称报错 二&#xff0c;错误分类 语法错误&#xff1a;书写的代码不符合 PHP 的语法规范&#xff0c;语法错…

【医学影像】LIDC-IDRI数据集的无痛制作

LIDC-IDRI数据集制作 0.下载0.0 链接汇总0.1 步骤 1.合成CT图reference 0.下载 0.0 链接汇总 LIDC-IDRI官方网址&#xff1a;https://www.cancerimagingarchive.net/nbia-search/?CollectionCriteriaLIDC-IDRINBIA Data Retriever 下载链接&#xff1a;https://wiki.canceri…

基于springboot+vue的编程训练系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

斯元Z-ONE-China Cybersecurity Tech Landscape·中国网络安全全景图-百度网盘下载

面向全球&#xff0c;斯元Z-ONE正式发布首版「China Cybersecurity Tech Landscape中国网络安全全景图」。 为了提升海外市场对中国网络安全行业的全局认识&#xff0c;方便国际客户及合作伙伴了解中国网络安全科技的赛道分布和国内外厂商对标&#xff0c;助力中国网安厂商出海…

Qt中tableView控件的使用

tableView使用注意事项 tableView在使用时&#xff0c;从工具栏拖动到底层页面后&#xff0c;右键进行选择如下图所示&#xff1a; 此处需要注意的是&#xff0c;需要去修改属性&#xff0c;从UI上修改属性如下所示&#xff1a; 也可以通过代码修改属性&#xff1a; //将其设…

个人玩航拍,如何申请无人机空域?

我们在《年会不能停》一文中&#xff0c;有分享我们在西岭雪山用无人机拍摄的照片和视频&#xff0c;有兴趣可以去回顾。 春节的时候&#xff0c;趁着回老家一趟&#xff0c;又将无人机带了回去&#xff0c;计划拍一下老家的风景。 原本以为穷乡僻壤的地方可以随便飞&#xf…

【c语言】内存函数

欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;欢迎指出~ 目录 memcpy函数的使用和模拟实现 memcpy函数的使用 memcpy函数的模拟实现 memmove的使用和模拟实现 memmove的使用 memmove的模拟实现 memset函数的使用 memcmp函数…

【Docker】安装及相关的命令

目录 一 Docker简介 1.1 是什么 1.2 优缺点 1.3 应用场景 1.4 安装 二 命令 2.1 Docker基本命令 2.2 Docker镜像命令 2.3 Docker容器命令 一 Docker简介 1.1 是什么 Docker是一个开源的应用容器引擎&#xff0c;它基于Go语言实现&#xff0c;并利用操作系统本身已有的…

Kafka安全模式之身份认证

一、简介 Kafka作为一个分布式的发布-订阅消息系统&#xff0c;在日常项目中被频繁使用&#xff0c;通常情况下无论是生产者还是消费者只要订阅Topic后&#xff0c;即可进行消息的发送和接收。而kafka在0.9.0.0版本后添加了身份认证和权限控制两种安全服务&#xff0c;本文主要…

六、防御保护---防火墙内容安全篇

六、防御保护---防火墙内容安全篇 一、IAE&#xff08;Intelligent Awareness Engine&#xff09;引擎二、深度检测技术(DFI和DPI&#xff09;2.1 DPI -- 深度包检测技术2.1.1 基于“特征字”的检测技术2.1.2 基于应用网关的检测技术2.1.3 基于行为模式的检测技术 2.2 DFI -- 深…

CGI程序与ShellShock漏洞

CGI是什么&#xff1f; CGI&#xff08;通用网关接口&#xff0c;Common Gateway Interface&#xff09;程序是一种用于在Web服务器上执行动态内容的技术。与服务器上普通的后端代码相比&#xff0c;CGI程序有几个区别&#xff1a; 执行环境&#xff1a; CGI程序在服务器上作为…

k8s部署java微服务程序时,关于配置conusl acl token的方法总结

一、背景 java微服务程序使用consul作为服务注册中心&#xff0c;而consul集群本身的访问是需要acl token的&#xff0c;以增强服务调用的安全性。 本文试着总结下&#xff0c;有哪些方法可以配置consul acl token&#xff0c;便于你根据具体的情况选择。 个人认为&#xff…

BL0942 内置时钟免校准计量芯片 用于智能家居领域 低成本

BL0939是上海贝岭股份有限公司开发的一款用于智能家居领域进行电能测量的专用芯片&#xff0c;支持两路测量&#xff0c;可同时进行计量和漏电故障检测&#xff0c;漏电检测电流可设&#xff0c;响应时间快&#xff0c;具有体积小&#xff0c;外围电路简单&#xff0c;成本低廉…

C++ 前缀和

目录 1、DP34 【模板】前缀和 2、DP35 【模板】二维前缀和​编辑 3、724. 寻找数组的中心下标 4、238. 除自身以外数组的乘积 5、560. 和为 K 的子数组 6、974. 和可被 K 整除的子数组 7、525. 连续数组 8、1314. 矩阵区域和 1、DP34 【模板】前缀和 思路&#xff1a;…

Project_Euler-11 题解

Project_Euler-11 题解 题目 题目中给出的数据如下&#xff1a; 08 02 22 97 38 15 00 40 00 75 04 05 07 78 52 12 50 77 91 08 49 49 99 40 17 81 18 57 60 87 17 40 98 43 69 48 04 56 62 00 81 49 31 73 55 79 14 29 93 71 40 67 53 88 30 03 49 13 36 65 52 70 95 23 04 …

ESP32语音转文字齐护百度在线语音识别

一、导入(10分钟&#xff09; 学习目的 二、新授(70分钟) 1.预展示结果(5分钟) 2.本节课所用的软硬件(5分钟) 4.图形化块介绍(10分钟) 5.单个模块的简单使用(10分钟) 6.在线语音转换工具逻辑分析(10分钟) 7.在线语音转换工具分步实现(30分钟) 三、巩固练习(5分钟) 四、课堂小结…

考研数据结构算法机试训练1

中南大学上机压轴题 测试数据&#xff1a; 3 500 0.6 100 0.8 200 0.7 100 输出 390首先要对输入的折扣进行排序&#xff0c;优先使用比率低的z进行支付。 然后用lowcost记录目前多少钱是打过折的。T-lowcost就是剩余没打折的。 每次循环用上一个人的折扣额度。若所有人折扣额…

VR转接器:破解虚拟与现实边界的革命性设备

VR转接器&#xff0c;这一革命性的设备&#xff0c;为虚拟现实体验带来了前所未有的自由度。它巧妙地连接了虚拟与现实&#xff0c;使得用户在享受VR眼镜带来的奇幻世界的同时&#xff0c;也能自由地在现实世界中活动。这一设计的诞生&#xff0c;不仅解决了VR眼镜续航的瓶颈问…

react-组件基础

1.目标 能够使用函数创建组件 能够使用class创建组件 能够给React元素绑定事件 能够使用state和setState() 能够处理事件中的this指向问题 能够使用受控组件方式处理表单 2.目录 React组件介绍 React组件的两种创建方式 React事件处理 有状态组件和无状态组件 组件中的state…