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

 更新时间:2022年10月29日 11:40:59   作者:龙骑士尹道长  
这篇文章主要为大家介绍了React为什么需要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调度器原理的资料请关注脚本之家其它相关文章!

相关文章

  • 详解在React项目中如何集成和使用web worker

    详解在React项目中如何集成和使用web worker

    在复杂的React应用中,某些计算密集型或耗时操作可能会阻塞主线程,导致用户界面出现卡顿或响应慢的现象,为了优化用户体验,可以采用Web Worker来在后台线程中执行这些操作,本文将详细介绍在React项目中如何集成和使用Web Worker来改善应用性能,需要的朋友可以参考下
    2023-12-12
  • 阿里低代码框架lowcode-engine自定义设置器详解

    阿里低代码框架lowcode-engine自定义设置器详解

    这篇文章主要为大家介绍了阿里低代码框架lowcode-engine自定义设置器示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-02-02
  • React immer与Redux Toolkit使用教程详解

    React immer与Redux Toolkit使用教程详解

    这篇文章主要介绍了React中immer与Redux Toolkit的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2022-10-10
  • react dva实现的代码

    react dva实现的代码

    dva是一个基于redux和redux-saga的数据流方案,然后为了简化开发体验,dva额外内置了react-router,fetch,可以激烈为一个轻量级的应用框架,这篇文章主要介绍了react dva实现,需要的朋友可以参考下
    2021-11-11
  • JavaScript React如何修改默认端口号方法详解

    JavaScript React如何修改默认端口号方法详解

    这篇文章主要介绍了JavaScript React如何修改默认端口号方法详解,文中通过步骤图片解析介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-07-07
  • 使用react-native-doc-viewer实现文档预览

    使用react-native-doc-viewer实现文档预览

    这篇文章主要介绍了使用react-native-doc-viewer实现文档预览,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-09-09
  • 关于React状态管理的三个规则总结

    关于React状态管理的三个规则总结

    随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态),这篇文章主要给大家介绍了关于React状态管理的三个规则,需要的朋友可以参考下
    2021-07-07
  • React类组件和函数组件对比-Hooks的简介

    React类组件和函数组件对比-Hooks的简介

    Hook 是 React 16.8 的新增特性,它可以让我们在不编写class的情况下, 使用state以及其他的React特性(比如生命周期,这篇文章主要介绍了React类组件和函数组件对比-Hooks的介绍及初体验,需要的朋友可以参考下
    2022-11-11
  • react+tsx中使用better-scroll详解

    react+tsx中使用better-scroll详解

    这篇文章主要介绍了react+tsx中使用better-scroll,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-09-09
  • 基于React实现一个todo打勾效果

    基于React实现一个todo打勾效果

    这篇文章主要为大家详细介绍了如何基于React实现一个todo打勾效果,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2024-03-03

最新评论