React16源码: React中的异步调度scheduler模块以及时间片源码实现

React Scheduler


1 ) 概述

  • react当中的异步调度,称为 React Scheduler
  • 发布成单独的一个 npm 包就叫做 scheduler
  • 这个包它做了什么?
    • A. 首先它维护时间片
    • B. 然后模拟 requestIdleCallback 这个API
      • 因为现在浏览器的支持不是特别的多
      • 所以在浏览当中只是去模拟了一个这个API,而不是直接使用这个API
      • 因为需要考虑到浏览器兼容性
      • 这个API的作用
        • 调用这个API传入一个回调之后,这个API会等到浏览器把它的一些主要任务执行完了
        • 当它有空闲的时间的时候,再回来调用这个回调
      • 相对于 requestAnimationFrame 来说,它的优先级会低很多
      • 它是等浏览器器要做的事情做完了之后,再回来调这个回调
      • requestAnimationFrame 是浏览器要渲染当前帧的时候,调用这个回调
    • C. 调度列表和进行一个超时的判断
  • 关于时间片
    • 不管是在浏览器还是在App当中,要给用户很流畅的一个感觉的时候
    • 至少要保证在一秒钟之内要渲染30帧以上
    • 现在的一些高刷新率的浏览器,可能会要求在60帧以上,甚至还有更高的,比如,120帧
    • 这个帧数就是我们1秒钟,页面要重新渲染刷新多少次
    • 它并不是说我一秒钟之内刷新30次,满足就行了。
    • 比如前面的半秒钟只刷新了一次,后面的半秒钟刷新了二十九次,这个也是不行的
    • 这个给用户的感觉,就是前面这半秒钟会特别的卡就一动不动,然后后面又变得流畅
    • 所以,它的要求还需要是平均的每33毫秒要刷新1帧,要保持这个频率
    • 浏览器必须自己去渲染这些动画,要每1帧里面有固定的时间去渲染这个动画
    • 在这里举个例子,比如说整个应用所有的js的操作,都是通过 react 来实现的
    • 而浏览器有一个一直在更新的动画, 浏览器渲染这个动画如果要11毫秒
    • 那么给每一帧的, 就是把一秒钟分成了30帧之后,每一帧是33毫秒
    • 这个33毫秒里面的11毫秒是必须要留给浏览器去渲染这个动画的, 才能让这个动画看起来是流畅的
    • 而在这个时候留给react去渲染它的应用更新的时候,每一帧里面就只有22毫秒
    • 如果react它在这一帧里面的一个更新,它需要渲染的时间很长,比如说35毫秒
    • 那这个时候,我们一帧的时间就全部给react渲染给占掉了
    • 因为 js 引擎是单线程的, 如果react在一直在执行,浏览器它就没有机会去获得运行权
    • 就没有机会去刷新它的一个动画, 这时候,不仅把一帧的时间占完了
    • 这样还不够,还要去下一帧里面借用一点时间,那么这个时间用完之后
    • 浏览器要去更新动画,如果这一帧里面我们就用掉了13毫秒,剩下的时间就只剩下20毫秒
    • 那么这20毫秒,又可能要运行一部分react的更新,然后再去浏览器的一个渲染
    • 这就会导致整个动画变得卡顿起来了
    • 这就是 React Scheduler 它的一个目的, 为了保证react它去执行更新的这个时间
    • 不超过在浏览器的每一帧里面特定的时间,它希望留给浏览器去刷新动画,或者是响应用户输入的反馈的时候
    • 每一帧里面有足够的时间

2 )时间片源码

  • 时间片源码在 packages/scheduler 这个包里面,是一个单独的模块,单独发布到 npm 上

  • 在 ReactFiberScheduler.js 里面,哪个地方用到它呢?

    • requestWork 函数里面,如果 expirationTime 异步的,就会调用 scheduleCallbackWithExpirationTime
      function scheduleCallbackWithExpirationTime(root: FiberRoot,expirationTime: ExpirationTime,
      ) {if (callbackExpirationTime !== NoWork) {// A callback is already scheduled. Check its expiration time (timeout).if (expirationTime > callbackExpirationTime) {// Existing callback has sufficient timeout. Exit.return;} else {if (callbackID !== null) {// Existing callback has insufficient timeout. Cancel and schedule a// new one.cancelDeferredCallback(callbackID);}}// The request callback timer is already running. Don't start a new one.} else {startRequestCallbackTimer();}callbackExpirationTime = expirationTime;const currentMs = now() - originalStartTimeMs;const expirationTimeMs = expirationTimeToMs(expirationTime);const timeout = expirationTimeMs - currentMs;callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
      }
      
      • 全局变量 callbackExpirationTime 对应的是 上一次调用 React Scheduler 去申请了一个callback
      • 这个callback 也会有一个 expirationTime, 因为是异步调度,所以会有一个 expirationTime 传进来
      • 如果这个 callbackExpirationTime !== NoWork 代表之前有一个callback在执行了
      • 这边就会判断当前的 expirationTime 是否比之前回调中的那个要大
      • 如果大,说明当前的这个的优先级要低,这个时候就直接return了不执行
      • 因为它优先级更低,我们肯定要执行优先级更高的那个,调用 cancelDeferredCallback 把之前的 cancel 掉
      • startRequestCallbackTimer 这个函数跳过,不涉及主流程,涉及DEV Tool 相关
      • 接着更新一系列的变量
        • 更新 callbackExpirationTime
        • 计算出 timeout
      • 最后调用 scheduleDeferredCallback 这个方法来自于 ReactFiberHostConfig.js
        • 如果直接查找 这个文件,发现基本上没有什么内容, 是因为 React对于打包工具的配置,进行了文件名的映射
        • 它实际映射的是 eact-reconciler/src/forks/ReactFiberHostConfig.dom.js
          export * from 'react-dom/src/client/ReactDOMHostConfig';
          
        • 发现里面就一行代码,找到对应的 ReactDOMHostConfig.js 文件,搜索 scheduleDeferredCallback 方法
          export {unstable_scheduleCallback as scheduleDeferredCallback,
          } from 'scheduler';
          
          • 可追溯到 这个方法来自于 scheduler 包
          • 这个方法涉及比较多,先跳过
        • callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
      • 它最后返回 一个 callbackID, 这个id用于后期 cancel 的标识,cancelDeferredCallback(callbackID);
        • 这里之前也说了,如果新的任务优先级更高,需要把老的取消,再调用新的callback
      • 而里面的参数 performAsyncWork
        • requestWork 中,当 expirationTime === Sync 时,调用的也是 performSyncWork 这个是同步的
        • 而如果是异步,则调用 scheduleCallbackWithExpirationTime 函数,最终调用的是这里的 performAsyncWork
        • 所以,这两个是对应的,同步和异步
  • 进入 scheduleDeferredCallback 函数的源码 packages/scheduler/src/Scheduler.js 找到 unstable_scheduleCallback

    function unstable_scheduleCallback(callback, deprecated_options) {var startTime =currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();var expirationTime;if (typeof deprecated_options === 'object' &&deprecated_options !== null &&typeof deprecated_options.timeout === 'number') {// FIXME: Remove this branch once we lift expiration times out of React.expirationTime = startTime + deprecated_options.timeout;} else {switch (currentPriorityLevel) {case ImmediatePriority:expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;break;case UserBlockingPriority:expirationTime = startTime + USER_BLOCKING_PRIORITY;break;case IdlePriority:expirationTime = startTime + IDLE_PRIORITY;break;case NormalPriority:default:expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;}}var newNode = {callback,priorityLevel: currentPriorityLevel,expirationTime,next: null,previous: null,};// Insert the new callback into the list, ordered first by expiration, then// by insertion. So the new callback is inserted any other callback with// equal expiration.if (firstCallbackNode === null) {// This is the first callback in the list.firstCallbackNode = newNode.next = newNode.previous = newNode;ensureHostCallbackIsScheduled();} else {var next = null;var node = firstCallbackNode;do {if (node.expirationTime > expirationTime) {// The new callback expires before this one.next = node;break;}node = node.next;} while (node !== firstCallbackNode);if (next === null) {// No callback with a later expiration was found, which means the new// callback has the latest expiration in the list.next = firstCallbackNode;} else if (next === firstCallbackNode) {// The new callback has the earliest expiration in the entire list.firstCallbackNode = newNode;ensureHostCallbackIsScheduled();}var previous = next.previous;previous.next = next.previous = newNode;newNode.next = next;newNode.previous = previous;}return newNode;
    }
    
    • 首先看 参数 callback, deprecated_options
      • callback 是传进来的 performAsyncWork
      • deprecated_options 是即将被废弃的 optinos,这个即将被废弃
    • 接着处理 var startTime = currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();
      • getCurrentTime 是重新计算一个 xx.now()
        if (hasNativePerformanceNow) {var Performance = performance;getCurrentTime = function() {return Performance.now();};
        } else {getCurrentTime = function() {return localDate.now();};
        }
        
      • 这里,浏览器平台是这个 localDate.now();
    • 下面有个判断if (typeof deprecated_options === 'object' && deprecated_options !== null && typeof deprecated_options.timeout === 'number')
      • 接着判断 deprecated_options 这个参数,存在则计算出 expirationTime
        // FIXME: Remove this branch once we lift expiration times out of React.
        expirationTime = startTime + deprecated_options.timeout;
        
        • 当把 expirationTime 相关的逻辑提取出来之后,这个 if判断就被删除了,后面只有 else 里面的东西了
        • 所以说,这个 deprecated_options 即将被废弃
    • 如果走到 else 里面,进行switch case currentPriorityLevel
      • 可以看下各个常量的值
        var maxSigned31BitInt = 1073741823;// Times out immediately
        var IMMEDIATE_PRIORITY_TIMEOUT = -1;
        // Eventually times out
        var USER_BLOCKING_PRIORITY = 250;
        var NORMAL_PRIORITY_TIMEOUT = 5000;
        // Never times out
        var IDLE_PRIORITY = maxSigned31BitInt;
        
      • 也就是说,将来很可能会把 expirationTime 相关逻辑移入 scheduler 包中
      • 之前在 packages/react-reconciler/src/ReactFiberReconciler.js 中
      • 不过,在目前的逻辑中 else 里面的东西,用不到
    • 接下去,创建 newNode 的对象
      var newNode = {callback,priorityLevel: currentPriorityLevel,expirationTime,next: null,previous: null,
      };
      
      • next 和 previous 是用来存储链表的数据结构的
    • 接下来 if (firstCallbackNode === null)
      • firstCallbackNode 是 scheduler 中维护的一个单项列表的头部
      • 如果匹配判断,说明传递进来的 callback 是第一个
        • 进行赋值处理 firstCallbackNode = newNode.next = newNode.previous = newNode;
        • 并调用 ensureHostCallbackIsScheduled();
    • 不匹配的时候
      • 有一个或多个callback, 则进行循环
      • 在循环中判断,node.expirationTime > expirationTime
        • 如果匹配,next = node; 并跳出循环
        • 这是 scheduler 对于传进来的所有callback, 按照 expirationTime 的大小,也就是优先级的高低进行排序
        • 它会把优先级更高的任务,排到最前面
      • 如果 next 是 null
        • 这个节点要插在callbackList里面的最后一个
      • 如果 next 是 firstCallbackNode,即第一个
        • 因为当前节点要插在这个单项列表最前面,优先级最高
        • 马上 firstCallbackNode 变化了,即更新了 firstCallbackNode = newNode;
        • 调用 ensureHostCallbackIsScheduled();
          • 这个函数在上面两处调用了,但是没有在 if (next === null) 中调用
            • 因为 这个条件下,firstCallbackNode 仍然处于第一位
            • 后续要调用的话,第一个被调用的还是 firstCallbackNode
            • 所以,顺序不会变,所以不需要重新调用 ensureHostCallbackIsScheduled();
          • 注意,调用上述方法会进入一个循环,循环的调用List里面的东西
          • 当 firstCallbackNode 变化了,才会去调用,因为头部变了
  • 下面为这个方法链表的处理示例

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

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

相关文章

使用scipy处理图片——旋转任意角度

大纲 载入图片左旋转30度,且重新调整图片大小右旋转30度,且重新调整图片大小左旋转135度,保持图片大小不变右旋转135度,保持图片大小不变 在《使用numpy处理图片——90度旋转》中,我们使用numpy提供的方法,…

【PostgreSQL】函数与操作符-逻辑操作符

PostgreSQL常用的操作符 算术操作符:PostgreSQL支持基本的算术操作符,包括加法()、减法(-)、乘法(*)、除法(/)和取模(%)等。可以使用这…

命令行登录Mysql的详细讲解

目录 前言1. 本地登录2. 远程登录3. 拓展 前言 对于命令行登录Mysql一般都是用mysql -u root -p 但对于如何远程登陆,一直其他的参数还是有些盲区,对此总结科普 对于登录过程中出现的问题,可看我之前的文章: 服务器 出现ERROR …

Dreambooth Stable Diffusion始化训练环境(AutoDL)

以AutoDL为例 以下代码源自:赛博华佗——秋叶: Akegarasu 环境选择 Miniconda: Miniconda是一个轻量级的Conda环境管理系统。它包含了conda、Python和一些常用的包,以及能够管理安装其他包的能力。Miniconda是Anaconda的一个简化版&#xff0…

聊聊PowerJob的Alarmable

序 本文主要研究一下PowerJob的Alarmable Alarmable tech/powerjob/server/extension/Alarmable.java public interface Alarmable {void onFailed(Alarm alarm, List<UserInfoDO> targetUserList); }Alarmable接口定义了onFailed方法&#xff0c;其入参为alarm及tar…

【矩阵快速幂】封装类及测试用例及样例

作者推荐 视频算法专题 通俗的说&#xff0c;就是矩阵的乘方。 封装类 核心代码 class CMat { public:// 矩阵乘法static vector<vector<long long>> multiply(const vector<vector<long long>>& a, const vector<vector<long long>…

使用Python管理MySQL数据库,有录播直播私教课视频教程

查看所有数据库 from mysql.db_object import Databasedb Database(host"localhost",port3306,database"",username"root",password"zhangdapeng520" ) print(db.get_all_database())删除数据库 from mysql.db_object import Datab…

Python 面向对象之元类

Python 面向对象之元类 【一】一切皆对象 【1】元类 元类&#xff08;metaclass&#xff09;是Python中用于创建类的类。在Python中&#xff0c;类是对象&#xff0c;而元类就是类的类它们控制类的创建过程&#xff0c;允许你定制类的行为Python中内置的默认元类是type我们用…

Android JNI与Java类的转换调用

1. 前言 一般在Android中&#xff0c;对于JNI的调用&#xff0c;基本的数据类型就能满足要求了&#xff0c;具体可以看我的这一篇文章 : Android JNI/NDK 入门从一到二&#xff0c;但是最近在项目中遇到了基本类型满足不了需要的情况&#xff0c;需要在JNI中创建并操作Java类&…

Rust-语句和表达式

if-else Rust中if-else表达式的作用是实现条件分支。if-else表达式的构成方式为&#xff1a;以if关键字开头&#xff0c;后面跟上条件表达式&#xff0c;后续是结果语句块&#xff0c;最后是可选的else块。条件表达式的类型必须是bool。 if-else结构还可以当表达式使用 loop …

如何用LLM和自有知识库搭建智能agent?

用LangChain建立知识库&#xff0c;文末中也推荐其他方案。 项目源码&#xff1a;ChatPDF实现 LangChain Indexes使用 对加载的内容进行索引&#xff0c;在indexes中提供了一些功能&#xff1a; Document Loaders&#xff0c;加载文档Text Splitters&#xff0c;文档切分V…

nautilus`文件管理器打开文件管理器

使用nautilus文件管理器打开文件管理器&#xff0c;并使用管理员权限。以下是使用命令行的步骤&#xff1a; 打开终端&#xff08;命令行界面&#xff09;&#xff0c;你可以通过按下Ctrl Alt T组合键来打开终端。 在终端中&#xff0c;使用以下命令以管理员权限打开文件管理…

开发实践5_后台管理^/ 分_页器

以下学习 朔宁夫 开发课 。&#xff08;Python&#xff09; 一 基本使用 创建超级用户 terminal // python manage.py createsuperuser 访问地址 //Log in | Django site adminhttp://127.0.0.1:8000/admin/login/?next/admin/ superuserr login django自带admin功能。其…

一文读懂「Prompt Engineering」提示词工程

在了解提示过程之前&#xff0c;先了解一下什么是提示prompt&#xff0c;见最后附录部分 一、什么是Prompt Engingering&#xff1f; 提示工程&#xff08;Prompt Engingering&#xff09;&#xff0c;也被称为上下文提示&#xff08;In-Context Prompting&#xff09;&#x…

Django项目中的默认文件都有什么用

manager.py&#xff1a; 是django用于管理本项目的命令行工具&#xff0c;之后进行站点运行&#xff0c;数据库自动生成等都是通过本文件完成。 djangoStudy/__init__.py&#xff1a; 告诉python该目录是一个python包&#xff0c;暂无内容&#xff0c;后期一些工具的初始化可…

【数据库】间隙锁Gap Lock

什么是间隙锁 间隙锁&#xff08;Gap Lock&#xff09;&#xff1a;间隙锁是&#xff08;RR级别下&#xff09;一个在索引记录之间的间隙上的锁&#xff0c;可以是两个索引记录之间&#xff0c;也可能是第一个索引记录之前或最后一个索引之后的空间。间隙锁&#xff08;Gap Lo…

Github登录时,要二次验证.Github 2FA登录问题

2023下半年, Github登录时,要二次验证. 点击“Enable 2FA now”跳到2FA验证界面。 点setup key是到secret串。 这里有一个可以不用app的验证方法。填入secret串,运行后,会得到验证码. https://blog.csdn.net/dejavu_980323/article/details/132318107 按照这个文章,得到验证…

记csv、parquet数据预览一个bug的解决

文章目录 一、概述二、实现过程1. 业务流程2. 业务逻辑3. 运行结果 三、bug现象1. 单元测试2.运行结果 三、解决思路1. 思路一2. 思路二 一、概述 工作中遇到通过sparksession解析csv、parquet文件并预览top100的需求。 二、实现过程 1. 业务流程 #mermaid-svg-Tzp1pJTvJunU…

227. Basic Calculator II

227. Basic Calculator II class Solution:def calculate(self, s: str) -> int:ititer(re.split(r([\\-\*\/]),s))lastansint(next(it))for op in it:nint(next(it))if op in -:if op-:n-nansnelse:if op*:nlast*nelse:nint(last/n)ansans-lastnlastnreturn ans 注意这个…

分布式任务调度平台XXL-JOB使用(二)

说明&#xff1a;之前总结过在CentOS系统里使用XXL-JOB。但在代码开发阶段&#xff0c;我们有时需要在本地环境测试定时任务代码&#xff0c;本文介绍如何在Windows系统使用XXL-JOB。 下载 &#xff08;1&#xff09;下载代码&#xff0c;解压打开 首先&#xff0c;去Github…