React为什么需要Scheduler调度器原理详解

目录
  • 正文
    • 我们为什么需要Scheduler(调度器)
    • Scheduler如何进行工作
  • 总结

正文

最近在重学React,由于近两年没使用React突然重学发现一些很有意思的概念,首先便是React的Scheduler(调度器) 由于我对React的概念还停留在React 15之前(就是那个没有hooks的年代),所以接触Scheduler(调度器) 让我感觉很有意思;

在我印象中React的架构分为两层(React 16 之前)

  • Reconciler(协调器)—— 负责找出变化的组件
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上

如今增加了Scheduler(调度器) ,那么调度器有什么用?调度器的作用是调度任务的优先级,高优任务优先进入Reconciler

我们为什么需要Scheduler(调度器)

要了解为什么需要Scheduler(调度器) 我们需要知道以下几个痛点;

  • React在何时进行更新;
  • 16之前的React怎样进行更新;
  • 16之前的React带来的痛点;

首先我们讲讲React何时进行更新,众所周知主流的浏览器的刷新频率是60HZ,也就是说主流的浏览器完成一次刷新需要1000/60 ms约等于16.666ms

然后我们需要知道浏览器在你开启一个页面的时候做了什么;总结下来就是一张图

CSSOM树的构建时机与JS的执行时机是依据你解析的link标签与script标签来确认的;因为当React开始更新时已完成部分工作(开始回流与重绘),所以经过精简,可以归为以下几个步骤

而以上的整个过程称之为一帧,通俗点讲就是在16.6ms之内(主流浏览器)js的事件循环进行完成之后会对页面进行渲染;那么React在何时对页面进行更新呢?react会在执行完以上整个过程之后的空闲时间进行更新,所以如果执行以上流程用了10ms则react会在余下的6.6ms内进行更新(一般5ms左右);

在React16之前组件的mount阶段会调用mountComponentupdate阶段会调用updateComponent,我们知道react的更新是从外向内进行更新,所以当时的做法是使用递归逐步更新子组件,而这个过程是不可中断的,所以当子组件嵌套层级过深则会出现卡顿,因为这个过程是同步不可中断的,所以react16之前采用的是同步更新策略,这显然不符合React的快速响应理念;

为了解决以上同步更新所带来的痛点,React16采用了异步可中断更新来替代它,所以在React16当中引入了Scheduler(调度器)

Scheduler如何进行工作

Scheduler主要包含两个作用

  • 时间切片
  • 优先级调度

关于时间切片很好理解,我们已经提到了Readt的更新会在重绘呈现之后的空闲时间执行;所以在本质上与requestIdleCallback 这个方法很相似;

requestIdleCallback(fn,timeout)

这个方法常用于处理一些优先级比较低的任务,任务会在浏览器空闲的时候执行而它有两个致命缺陷

  • 不是所有浏览器适用(兼容性)
  • 触发不稳定,在浏览器FPS为20左右的时候会比较流畅(违背React快速响应)

因此React放弃了requestIdleCallback 而实现了功能更加强大的requestIdleCallback polyfill 也就是 Scheduler

首先我们看下JS在浏览器中的执行流程与requestIdleCallback的执行时机

Scheduler的时间切片将以回调函数的方式在异步宏任务当中执行;请看源码

var schedulePerformWorkUntilDeadline;
//node与旧版IE中执行
if (typeof localSetImmediate === 'function') {
  // Node.js and old IE.
  // There's a few reasons for why we prefer setImmediate.
  //
  // Unlike MessageChannel, it doesn't prevent a Node.js process from exiting.
  // (Even though this is a DOM fork of the Scheduler, you could get here
  // with a mix of Node.js 15+, which has a MessageChannel, and jsdom.)
  // https://github.com/facebook/react/issues/20756
  //
  // But also, it runs earlier which is the semantic we want.
  // If other browsers ever implement it, it's better to use it.
  // Although both of these would be inferior to native scheduling.
  schedulePerformWorkUntilDeadline = function () {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  //判断浏览器能否执行MessageChannel对象,同属异步宏任务,优先级高于setTimeout
  // DOM and Worker environments.
  // We prefer MessageChannel because of the 4ms setTimeout clamping.
  var channel = new MessageChannel();
  var port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = function () {
    port.postMessage(null);
  };
} else {
  //如果当前非旧IE与node环境并且不具备MessageChannel则使用setTimeout执行回调函数
  // We should only fallback here in non-browser environments.
  schedulePerformWorkUntilDeadline = function () {
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}

可以看到Scheduler在使用了三种异步宏任务方式,在旧版IE与node环境中使用setImmediate,在一般情况下使用MessageChannel如果当前环境不支持MessageChannel则改用setTimeout

那么讲完时间切片,我们来讲讲调度优先级;首先我们要知道对应的五种优先级

// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;//已经过期
// Eventually times out
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;//将要过期
var NORMAL_PRIORITY_TIMEOUT = 5000;//一般优先级任务
var LOW_PRIORITY_TIMEOUT = 10000;//低优先级任务
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;//最低优先级

可以看到过期时长越低的任务优先级越高,Scheduler是根据任务优先级情况来调度的,它会优先调度优先级高的任务,再调度优先级低的任务,如果在调度低优先级任务时突然插入一个高优先级任务则会中断并保存该任务让高优先级任务插队,在之后有空闲时间片再从队列中取出执行;我们来看主入口函数unstable_scheduleCallback

function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = exports.unstable_now();
  var startTime;
   //获取任务延迟
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
       //延迟任务
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }
  var timeout;
  //根据不同优先级对应时间给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;
  //新任务初始化
  var newTask = {
    id: taskIdCounter++,
    callback: callback,
    priorityLevel: priorityLevel,
    startTime: startTime,
    expirationTime: expirationTime,
    sortIndex: -1
  };
   //如果startTime大于currentTime则说明优先级低,为延迟任务
  if (startTime > currentTime) {
    // This is a delayed task.
    //将startTime存入新任务,用于任务排序(执行顺序)
    newTask.sortIndex = startTime;
    //采用小顶堆,将新任务插入延迟任务队列进行排序
    //当前startTime > currentTime所以当前任务为延迟任务插入延迟任务队列
    push(timerQueue, newTask);
    //若可执行任务队列为空或者新任务为延迟任务的第一个
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        //取消延时调度
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      } // Schedule a timeout.
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    newTask.sortIndex = expirationTime;
    //推入可执行队列
    push(taskQueue, newTask);
    // wait until the next time we yield.
    //当前可调度无插队任务
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);//执行
    }
  }
  return newTask;
}

从代码中可以看到Scheduler中的任务以队列的形式进行保存分别是 可执行队列taskQueue延迟队列timerQueue 当新任务进入方法unstable_scheduleCallback会将任放到延迟队列timerQueue中进行排序(优先级依照任务的sortIndex),如果延迟队列timerQueue中有任务变成可执行状态(currentTmie>startTime)则我们会将任务放入我们会将任务取出并放入可执行队列taskQueue并取出最快到期的任务执行

总结

React是以异步可中断的更新来替代原有的同步更新,而实现异步可中断更新的关键是SchedulerScheduler主要的功能是时间切片优先级调度,实现时间切片的关键是requestIdleCallback polyfill,调度任务为异步宏任务。而实现优先级调度的关键是当前任务到期时间,到期时间短的优先级更高,根据任务的优先级分别保存在可执行队列延时队列

以上就是React为什么需要Scheduler调度器原理详解的详细内容,更多关于React Scheduler调度器原理的资料请关注我们其它相关文章!

(0)

相关推荐

  • react Scheduler 实现示例教程

    目录 正文 简单的css动画 etTimeout来实现 循环处理 具体思路 正文 最近在看react源码,react构建fiber树这一块逻辑还比较好理解,但是一旦涉及到任务调度相关的逻辑,看起来是一头雾水.在参考了一些资料和react scheduler源码后,我决定来实现一个简单版的scheduler,相信跟着本文的思路实现一遍,就可以理解为什么react需要有scheduler这个东西来调度任务. 简单的背景知识: 我们知道现在大部分设备的帧率都是60fps,也就是说浏览器每16.7ms会

  • 深入理解React调度(Scheduler)原理

    目录 异步调度 时间分片 异步调度原理 总结 异步调度 问题:由于对于大型的 React 应用,会存在一次更新,递归遍历大量的虚拟 DOM ,造成占用 js 线程,使得浏览器没有时间去做一些动画效果,伴随项目越来越大,项目会越来越卡. 对比Vue: Vue 有这 template 模版收集依赖的过程,轻松构建响应式,使得在一次更新中,Vue 能够迅速响应,找到需要更新的范围,然后以组件粒度更新组件,渲染视图. React 中,一次更新 React 无法知道此次更新的波及范围,所以 React 选

  • React为什么需要Scheduler调度器原理详解

    目录 正文 我们为什么需要Scheduler(调度器) Scheduler如何进行工作 总结 正文 最近在重学React,由于近两年没使用React突然重学发现一些很有意思的概念,首先便是React的Scheduler(调度器) 由于我对React的概念还停留在React 15之前(就是那个没有hooks的年代),所以接触Scheduler(调度器) 让我感觉很有意思: 在我印象中React的架构分为两层(React 16 之前) Reconciler(协调器)—— 负责找出变化的组件 Rend

  • Spark调度架构原理详解

    1.启动spark集群,就是执行sbin/start-all.sh,启动master和多个worker节点,master主要作为集群的管理和监控,worker节点主要担任运行各个application的任务.master节点需要让worker节点汇报自身状况,比如CPU,内存多大,这个过程都是通过心跳机制来完成的 2.master收到worker的汇报信息之后,会给予worker信息 3.driver提交任务给spark集群[driver和master之间的通信是通过AKKAactor来做的,也

  • laravel异步监控定时调度器实例详解

    定时调度器是什么 laravel默认提供了一个命令定时任务的功能,在其他的php框架下面,没有这个定时任务,我们要跑一些异步脚本怎么操作呢,只能依赖我们系统提供的crontab来做,这就导致我们每次发版本新增定时任务都要去服务器更改crontab代码,获取更新这个配置. 执行命令是php artisan schedule:run 来执行,那放在哪里执行呢,没错这个调起还是需要依赖我们crontab来执行,但是只需要配置一次,后续所有定时任务都在我们业务代码进行控制 场景 我们有一个导入数据的定时

  • React开发进阶redux saga使用原理详解

    目录 前言 redux的特点 分析原理 1. 自动执行Generator 2. 发布订阅模式 3. put, takeEvery, delay, call返回effect 总结 前言 工作中使用了redux-saga这个redux中间件,如果不明白内部原理使用起来会让人摸不着头脑,阅读源码后特意对其原理做下总结. redux的特点 一个标准.管理应用副作用的redux中间件 实现切面编程方式 声明式的编写方式 订阅发布的设计模式 优点: 把异步操作转移到单独 saga文件中,而不是糅杂在acti

  • 简单的Python调度器Schedule详解

    最近在做项目的时候经常会用到定时任务,由于我的项目是使用Java来开发,用的是SpringBoot框架,因此要实现这个定时任务其实并不难. 后来我在想如果我要在Python中实现,我要怎么做呢? 一开始我首先想到的是Timer Timer 这个是一个扩展自threading模块来实现的定时任务.它其实是一个线程. # 首先定义一个需要定时执行的方法 >>> def hello(): print("hello!") # 导入threading,并创建Timer,设置1秒

  • python装饰器的特性原理详解

    这篇文章主要介绍了python装饰器的特性原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 今天发现了装饰器的另一种用法,下面就先上代码: data_list = [] def data_item(func): data_list.append(func) return func @data_item def foo(): return 1 @data_item def foo1(): return 2 @data_item def fo

  • React Context源码实现原理详解

    目录 什么是 Context Context 使用示例 createContext Context 的设计非常特别 useContext useContext 相关源码 debugger 查看调用栈 什么是 Context 目前来看 Context 是一个非常强大但是很多时候不会直接使用的 api.大多数项目不会直接使用 createContext 然后向下面传递数据,而是采用第三方库(react-redux). 想想项目中是不是经常会用到 @connect(...)(Comp) 以及 <Pro

  • JavaScript装饰器的实现原理详解

    目录 装饰器的常见作用 装饰类的属性 装饰类 注意 实例应用 最近在使用TS+Vue的开发模式,发现项目中大量使用了装饰器,看得我手足无措,今天特意研究一下实现原理,方便自己理解这块知识点. 装饰器的常见作用 装饰一个类的属性 装饰一个类 装饰器只能针对类和类的属性,不能直接作用于函数,因为存在函数提升. 下面我们针对这两种情况进行举例阐述. 装饰类的属性 function readonly(target, name, descriptor) { discriptor.writable = fa

  • Java定时任务原理详解

    目录 序章 一.Scheduled 1.1 使用方法 1.2 源码分析 二.QUARTZ 2.1 使用方法 2.2 源码分析 序章 定时任务实现方式 当下,java编码过程中,实现定时任务的方式主要以以下两种为主 spring框架的@Scheduled quzrtz框架 网络上关于这两种框架的实践和配置相关的教程很多,这里不再赘述. 本文主要就二者的框架原理实现做一个入门引导,为了解深层实现细节做一定的铺垫. 本文源码版本 spring-context-3.2.18.RELEASE.jar qu

  • Kotlin协程launch启动流程原理详解

    目录 1.launch启动流程 反编译后的Java代码 2.协程是如何被启动的 1.launch启动流程 已知协程的启动方式之一是Globalscope.launch,那么Globalscope.launch的流程是怎样的呢,直接进入launch的源码开始看起. fun main() { coroutineTest() Thread.sleep(2000L) } val block = suspend { println("Hello") delay(1000L) println(&q

随机推荐