【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. 根据传入的优先级, 设置任务的过期时间 expirationTimevar 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树太大, 出现了中断渲染), 保留currentTaskcurrentTask.callback = continuationCallback;} else {// 把currentTask移出队列if (currentTask === peek(taskQueue)) {pop(taskQueue);}}} else {// 如果任务被取消(这时currentTask.callback = null), 将其移出队列pop(taskQueue);}// 更新currentTaskcurrentTask = 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. 保存callbackscheduledHostCallback = 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();// 更新deadlinedeadline = currentTime + yieldInterval;// 执行callbackscheduledHostCallback(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.mzph.cn/news/706385.shtml

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

相关文章

4核8G服务器多少钱?腾讯云和阿里云哪家便宜?

4核8G云服务器多少钱一年&#xff1f;阿里云ECS服务器u1价格955.58元一年&#xff0c;腾讯云轻量4核8G12M带宽价格是646元15个月&#xff0c;阿腾云atengyun.com整理4核8G云服务器价格表&#xff0c;包括一年费用和1个月收费明细&#xff1a; 云服务器4核8G配置收费价格 阿里…

网站的安全防护需要注意哪些问题?有什么方法可以加固网站的防护

网站的安全防护&#xff0c;是一项复杂性、多方面的系统工程。现如今网络安全风险的增加&#xff0c;使得上至国家部门机关&#xff0c;小到个人博客&#xff0c;都有可能遭受网络安全问题。说到网络安全问题&#xff0c;比如&#xff1a;竞争最为激烈的游戏行业&#xff0c;从…

MySQL数据库进阶第六篇(InnoDB引擎架构,事务原理,MVCC)

文章目录 一、InnoDB引擎逻辑储存结构二、架构——内存结构三、架构——磁盘结构四、架构——后台线程五、事务原理持久性&#xff1a;redo log。重做日志原子性&#xff1a;undo log。回滚日志 六、MVCC基本概念七、MVCC实现原理八、undo log日志 回滚日志&#xff0c;版本链九…

shell中正则表达式讲解

1. 概念 在进行程序设计的过程中&#xff0c;用户会不可避免地遇到处理某些文本的情况。有的 时候&#xff0c;用户还需要查找符合某些比较复杂规则的字符串。对于这些情况&#xff0c;如果 单纯依靠程序设计语言本身&#xff0c;则往往会使得用户通过复杂的代码来实现。但 是&…

在linux上不依赖于Nignx等服务器部署ASP.NET Core 7.0 WebAPI

笔者近期需要部署一款基于B/S架构的后端程序在linux的Debian发行版上&#xff0c;本文章以本次部署遇到的问题为线索&#xff0c;总结如何在Debian上部署ASP.NET Core7.0WebAPI应用程序。 在linux上不依赖于Nignx等服务器部署ASP.NET Core 7.0 WebAPI 1.先决条件2.应用发布3.部…

3D可视化项目,选择unity3D还是three.js,是时候挑明了。

2023-08-10 23:07贝格前端工场 Hi&#xff0c;我是贝格前端工场&#xff0c;在开发3D可视化项目中&#xff0c;是选择U3D还是three,js时&#xff0c;很多老铁非常的迷茫&#xff0c;本文给老铁们讲清楚该如何选择&#xff0c;欢迎点赞评论分享转发。 一、Unity3D和three.js简…

[Mac软件]Adobe Substance 3D Stager 2.1.4 3D场景搭建工具

应用介绍 Adobe Substance 3D Stager&#xff0c;您设备齐全的虚拟工作室。在这个直观的舞台工具中构建和组装 3D 场景。设置资产、材质、灯光和相机。导出和共享媒体&#xff0c;从图像到 Web 和 AR 体验。 处理您的最终图像 Substance 3D Stager 可让您在上下文中做出创造性…

网络原理——HTTPS

HTTPS是 在HTTP的基础上&#xff0c;引入了一个加密层&#xff08;SSL)。 1. 为什么需要HTTPS 在我们使用浏览器下载一些软件时&#xff0c;相信大家都遇到过这种情况&#xff1a;明明这个链接显示的是下载A软件&#xff0c;点击下载时就变成了B软件&#xff0c;这种情况是运…

计算机设计大赛 深度学习手势检测与识别算法 - opencv python

文章目录 0 前言1 实现效果2 技术原理2.1 手部检测2.1.1 基于肤色空间的手势检测方法2.1.2 基于运动的手势检测方法2.1.3 基于边缘的手势检测方法2.1.4 基于模板的手势检测方法2.1.5 基于机器学习的手势检测方法 3 手部识别3.1 SSD网络3.2 数据集3.3 最终改进的网络结构 4 最后…

深入理解Python中的JSON模块:基础大总结与实战代码解析【第102篇—JSON模块】

深入理解Python中的JSON模块&#xff1a;基础大总结与实战代码解析 在Python中&#xff0c;JSON&#xff08;JavaScript Object Notation&#xff09;模块是处理JSON数据的重要工具之一。JSON是一种轻量级的数据交换格式&#xff0c;广泛应用于Web开发、API通信等领域。本文将…

WinForms中的Timer探究:Form Timer与Thread Timer的差异

WinForms中的Timer探究&#xff1a;Form Timer与Thread Timer的差异 在Windows Forms&#xff08;WinForms&#xff09;应用程序开发中&#xff0c;定时器&#xff08;Timer&#xff09;是一个常用的组件&#xff0c;它允许我们执行定时任务&#xff0c;如界面更新、周期性数据…

osi模型,tcp/ip模型(名字由来+各层介绍+中间设备介绍)

目录 网络协议如何分层 引入 osi模型 tcp/ip模型 引入 命名由来 介绍 物理层 数据链路层 网络层 传输层 应用层 中间设备 网络协议如何分层 引入 我们已经知道了网络协议是层状结构,接下来就来了解了解下网络协议如何分层 常见的网络协议分层模型是OSI模型 和 …

windows 连接 Ubuntu 失败 -- samba服务

1. windows10连接ubuntu的时候&#xff0c;提示不允许一个用户使用一个以上用户名与服务器或共享资源的多重连接&#xff0c;中断与此服务器或共享资源的所有连接&#xff0c;然后再试一次 2. 换一台同事的电脑却又可以连上&#xff0c;我之前一直能用的&#xff0c;隔一段时间…

UE5 C++ 单播 多播代理 动态多播代理

一. 代理机制&#xff0c;代理也叫做委托&#xff0c;其作用就是提供一种消息机制。 发送方 &#xff0c;接收方 分别叫做 触发点和执行点。就是软件中的观察者模式的原理。 创建一个C Actor作为练习 二.单播代理 创建一个C Actor MyDeligateActor作为练习 在MyDeligateAc…

【蓝桥杯】包子凑数(DP)

一.题目描述 二.输入描述 三.输出描述 四.问题分析 几个两两互质的数&#xff0c;最大公约数是1&#xff0c;最小公倍数是他们的乘积。 两个互质的数a和b最小不能表示的数就是&#xff08;a-1&#xff09;&#xff08;b-1&#xff09;-1&#xff0c;即&#xff0c;两个互质的数…

uniapp_微信小程序日历

一、需求要求这样 二、代码实现 <view class"calender" click"showriliall"><text class"lineText">探视日期&#xff1a;</text><text class"middleText">{{timerili}}</text><image src"/s…

Ubuntu服务器fail2ban的使用

作用&#xff1a;限制ssh远程登录&#xff0c;防止被人爆破服务器&#xff0c;封禁登录ip 使用lastb命令可查看到登录失败的用户及ip&#xff0c;无时无刻的不在爆破服务器 目录 一、安装fail2ban 二&#xff0c;配置fail2ban封禁ip的规则 1&#xff0c;进入目录并创建ssh…

【c++leetcode】1382. Balance a Binary Search Tree

问题入口 DSW (DAY, STOUT & WARREN) ALGORITHM 时间复杂度O(n) class Solution { public:int makeVine(TreeNode* grand, int cnt 0){auto n grand->right;while (n ! nullptr){if(n->left ! nullptr){auto old_n n;n n->left;old_n->left n->righ…

力扣hot100题解(python版18-21题)

18、矩阵置零 给定一个 *m* x *n* 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用 原地 算法**。** 示例 1&#xff1a; 输入&#xff1a;matrix [[1,1,1],[1,0,1],[1,1,1]] 输出&#xff1a;[[1,0,1],[0,0,0],[1,0,1]]示例…

Stable Diffusion WebUI 折腾新篇章

原文&#xff1a;https://blog.iyatt.com/?p13123 1 前言 第一次玩 Stable Diffusion WebUI 是三十几天前&#xff0c;当时还在用四年半前&#xff08;大学前暑假&#xff09;买的轻薄本&#xff0c;而在半年前独显还坏了&#xff0c;所以是纯纯的用 CPU 折腾&#xff0c;刚…