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

news/2024/7/15 17:56:10 标签: react.js, 前端, 前端框架

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.niftyadmin.cn/n/5325275.html

相关文章

排序算法7----归并排序(C语言)

1、基本思想 归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。 将无序的序列分解到单个元素,然后将有序的子序列合并,得到完全有序的序列:即先使每个子序列有序,再使子序列段…

C++ 11新特性之Lambda表达式

概述 在C的发展历程中,C 11版本无疑是一次重大飞跃,它引入了众多现代化和便利性的新特性。其中,Lambda表达式的出现极大地提升了代码的可读性和简洁性,使得函数对象的定义变得更加直观且易于使用。本文将深入探讨C 11中的Lambda表…

Mac M1 Parallels CentOS7.9 Rancher + K8S + Gitlab + Jenkins +Harbor CICD

一、资源清单 机器名称IP地址角色k8srancher高可用部署: https://blog.csdn.net/qq_41594280/article/details/135312148rancher10.211.55.200管理K8S集群k8svip10.211.55.199K8S VIPmaster0110.211.55.201K8S集群主节点master0210.211.55.202K8S集群主节点master0310.211.55.…

命令行登录Mysql的详细讲解

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

Ubuntu共享文件到win

Ubuntu共享文件到win 1、安装samba sudo apt-get install samba samba-common2、创建一个共享文件夹,并设置777权限 mkdir /home/qyh/share sudo chmod 777 /home/qyh/share我的用户名:qyh。 3、添加用户及密码 sudo smbpasswd -a qyh4、修改配置文…

云原生专栏大纲

1. 私有云实战之基础环境搭建 2. 云原生实战之kubesphere搭建 3.云原生之kubesphere运维 4. 云原生之kubesphere基础服务搭建 5.云原生安全之kubesphere应用网关配置域名TLS证书 6.云原生之DevOps和CICD 7.云原生之jenkins集成SonarQube 8.云原生存储之Ceph集群 9.云原生存储之…

基于冯·诺依曼架构能否实现有主动思想的AI

冯诺依曼架构是一种计算机体系结构,由冯诺依曼在20世纪40年代设计并提出。它是现代计算机设计的基础,并广泛应用于当前的计算机系统。冯诺依曼架构的核心思想是将程序和数据存储在同一个存储器中,并通过控制单元、算术逻辑单元、存储器和输入…

Clickhouse表引擎之CollapsingMergeTree引擎的原理与使用

前言 继续上次关于clickhouse的一些踩坑点,今天讲讲另外一个表引擎——CollapsingMergeTree。这个对于引擎对于数据量较大的场景是个不错的选择。注意,选择clickhouse的一般原因都是为了高效率查询,提高用户体验感,说白了就是以空…