点击进入React源码调试仓库。

Scheduler作为一个独立的包,能够单独承当起任务调度的职责,你只须要将工作和工作的优先级交给它,它就能够帮你治理工作,安顿工作的执行。这就是React和Scheduler配合工作的模式。

对于多个工作,它会先执行优先级高的。聚焦到单个工作的执行上,会被Scheduler有节制地去执行。换句话说,线程只有一个,它不会始终占用着线程去执行工作。而是执行一会,中断一下,如此往返。用这样的模式,来防止始终占用无限的资源执行耗时较长的工作,解决用户操作时页面卡顿的问题,实现更快的响应。

咱们能够从中梳理出Scheduler中两个重要的行为:多个工作的治理单个工作的执行管制

基本概念

为了实现上述的两个行为,它引入两个概念:工作优先级工夫片

工作优先级让工作依照本身的紧急水平排序,这样能够让优先级最高的工作最先被执行到。

工夫片规定的是单个工作在这一帧内最大的执行工夫,工作一旦执行工夫超过工夫片,则会被打断,有节制地执行工作。这样能够保障页面不会因为工作间断执行的工夫过长而产生卡顿。

原理概述

基于工作优先级和工夫片的概念,Scheduler围绕着它的外围指标 - 任务调度,衍生出了两大外围性能:工作队列治理 和 工夫片下工作的中断和复原。

工作队列治理

工作队列治理对应了Scheduler的多任务治理这一行为。在Scheduler外部,把工作分成了两种:未过期的和已过期的,别离用两个队列存储,前者存到timerQueue中,后者存到taskQueue中。

如何辨别工作是否过期?

用工作的开始工夫(startTime)和以后工夫(currentTime)作比拟。开始工夫大于以后工夫,阐明未过期,放到timerQueue;开始工夫小于等于以后工夫,阐明已过期,放到taskQueue。

不同队列中的工作如何排序?

当工作一个个入队的时候,天然要对它们进行排序,保障紧急的工作排在后面,所以排序的根据就是工作的紧急水平。而taskQueue和timerQueue中工作紧急水平的断定规范是有区别的。

  • taskQueue中,根据工作的过期工夫(expirationTime)排序,过期工夫越早,阐明越紧急,过期工夫小的排在后面。过期工夫依据工作优先级计算得出,优先级越高,过期工夫越早。
  • timerQueue中,根据工作的开始工夫(startTime)排序,开始工夫越早,说明会越早开始,开始工夫小的排在后面。工作进来的时候,开始工夫默认是以后工夫,如果进入调度的时候传了延迟时间,开始工夫则是以后工夫与延迟时间的和。

工作入队两个队列,之后呢?

如果放到了taskQueue,那么立刻调度一个函数去循环taskQueue,挨个执行外面的工作。

如果放到了timerQueue,那么阐明它外面的工作都不会立刻执行,那就等到了timerQueue外面排在第一个工作的开始工夫,看这个工作是否过期,如果是,则把工作从timerQueue中拿进去放入taskQueue,调度一个函数去循环它,执行掉外面的工作;否则过一会持续查看这第一个工作是否过期。

工作队列治理绝对于单个工作的执行,是宏观层面的概念,它利用工作的优先级去治理工作队列中的工作程序,始终让最紧急的工作被优先解决。

单个工作的中断以及复原

单个工作的中断以及复原对应了Scheduler的单个工作执行管制这一行为。在循环taskQueue执行每一个工作时,如果某个工作执行工夫过长,达到了工夫片限度的工夫,那么该工作必须中断,以便于让位给更重要的事件(如浏览器绘制),等事件实现,再复原执行工作。

例如这个例子,点击按钮渲染140000个DOM节点,为的是让React通过scheduler调度一个耗时较长的更新工作。同时拖动方块,这是为了模仿用户交互。更新工作会占用线程去执行工作,用户交互要也要占用线程去响应页面,这就决定了它们两个是互斥的关系。在React的concurrent模式下,通过Scheduler调度的更新工作遇到用户交互之后,会是上面动图里的成果。

执行React工作和页面响应交互这两件事件是互斥的,但因为Scheduler能够利用工夫片中断React工作,而后让出线程给浏览器去绘制,所以一开始在fiber树的构建阶段,拖动方块会失去及时的反馈。然而前面卡了一下,这是因为fiber树构建实现,进入了同步的commit阶段,导致交互卡顿。剖析页面的渲染过程能够十分直观地看到通过工夫片的管制。主线程被让进来进行页面的绘制(Painting和Rendering,绿色和紫色的局部)。

Scheduler要实现这样的调度成果须要两个角色:工作的调度者、工作的执行者。调度者调度一个执行者,执行者去循环taskQueue,一一执行工作。当某个工作的执行工夫比拟长,执行者会依据工夫片中断工作执行,而后通知调度者:我当初正执行的这个工作被中断了,还有一部分没实现,但当初必须让位给更重要的事件,你再调度一个执行者吧,好让这个工作能在之后被继续执行完(工作的复原)。于是,调度者晓得了工作还没实现,须要持续做,它会再调度一个执行者去持续实现这个工作。

通过执行者和调度者的配合,能够实现工作的中断和复原。

原理小结

Scheduler治理着taskQueue和timerQueue两个队列,它会定期将timerQueue中的过期工作放到taskQueue中,而后让调度者告诉执行者循环taskQueue执行掉每一个工作。执行者管制着每个工作的执行,一旦某个工作的执行工夫超出工夫片的限度。就会被中断,而后以后的执行者登场,登场之前会告诉调度者再去调度一个新的执行者持续实现这个工作,新的执行者在执行工作时依旧会依据工夫片中断工作,而后登场,反复这一过程,直到以后这个工作彻底实现后,将工作从taskQueue出队。taskQueue中每一个工作都被这样解决,最终实现所有工作,这就是Scheduler的残缺工作流程。

这外面有一个关键点,就是执行者如何晓得这个工作到底实现没实现呢?这是另一个话题了,也就是判断工作的实现状态。在解说执行者执行工作的细节时会重点突出。

以上是Scheduler原理的概述,上面开始是对React和Scheduler联结工作机制的具体解读。波及React与Scheduler的连贯、调度入口、工作优先级、工作过期工夫、工作中断和复原、判断工作的实现状态等内容。

具体流程

在开始之前,咱们先看一下React和Scheduler它们二者形成的一个零碎的示意图。

整个零碎分为三局部:

  • 产生工作的中央:React
  • React和Scheduler交换的翻译者:SchedulerWithReactIntegration
  • 工作的调度者:Scheduler

React中通过上面的代码,让fiber树的构建工作进入调度流程:

scheduleCallback(  schedulerPriorityLevel,  performConcurrentWorkOnRoot.bind(null, root),);

工作通过翻译者交给Scheduler,Scheduler进行真正的任务调度,那么为什么须要一个翻译者的角色呢?

React与Scheduler的连贯

Scheduler帮忙React调度各种工作,然而实质上它们是两个齐全不耦合的货色,二者各自都有本人的优先级机制,那么这时就须要有一个两头角色将它们连接起来。

实际上,在react-reconciler中提供了这样一个文件专门去做这样的工作,它就是SchedulerWithReactIntegration.old(new).js。它将二者的优先级翻译了一下,让React和Scheduler能读懂对方。另外,封装了一些Scheduler中的函数供React应用。

在执行React工作的重要文件ReactFiberWorkLoop.js中,对于Scheduler的内容都是从SchedulerWithReactIntegration.old(new).js导入的。它能够了解成是React和Scheduler之间的桥梁。

// ReactFiberWorkLoop.jsimport {  scheduleCallback,  cancelCallback,  getCurrentPriorityLevel,  runWithPriority,  shouldYield,  requestPaint,  now,  NoPriority as NoSchedulerPriority,  ImmediatePriority as ImmediateSchedulerPriority,  UserBlockingPriority as UserBlockingSchedulerPriority,  NormalPriority as NormalSchedulerPriority,  flushSyncCallbackQueue,  scheduleSyncCallback,} from './SchedulerWithReactIntegration.old';

SchedulerWithReactIntegration.old(new).js通过封装Scheduler的内容,对React提供两种调度入口函数:scheduleCallbackscheduleSyncCallback。工作通过调度入口函数进入调度流程。

例如,fiber树的构建工作在concurrent模式下通过scheduleCallback实现调度,在同步渲染模式下由scheduleSyncCallback实现。

// concurrentMode// 将本次更新工作的优先级转化为调度优先级// schedulerPriorityLevel为调度优先级const schedulerPriorityLevel = lanePriorityToSchedulerPriority(  newCallbackPriority,);// concurrent模式scheduleCallback(  schedulerPriorityLevel,  performConcurrentWorkOnRoot.bind(null, root),);// 同步渲染模式scheduleSyncCallback(  performSyncWorkOnRoot.bind(null, root),)

它们两个其实都是对Scheduler中scheduleCallback的封装,只不过传入的优先级不同而已,前者是传递的是曾经本次更新的lane计算得出的调度优先级,后者传递的是最高级别的优先级。另外的区别是,前者间接将工作交给Scheduler,而后者先将工作放到SchedulerWithReactIntegration.old(new).js本人的同步队列中,再将执行同步队列的函数交给Scheduler,以最高优先级进行调度,因为传入了最高优先级,意味着它将会是立刻过期的工作,会立刻执行掉它,这样可能保障在下一次事件循环中执行掉工作。

function scheduleCallback(  reactPriorityLevel: ReactPriorityLevel,  callback: SchedulerCallback,  options: SchedulerCallbackOptions | void | null,) {  // 将react的优先级翻译成Scheduler的优先级  const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);  // 调用Scheduler的scheduleCallback,传入优先级进行调度  return Scheduler_scheduleCallback(priorityLevel, callback, options);}function scheduleSyncCallback(callback: SchedulerCallback) {  if (syncQueue === null) {    syncQueue = [callback];    // 以最高优先级去调度刷新syncQueue的函数    immediateQueueCallbackNode = Scheduler_scheduleCallback(      Scheduler_ImmediatePriority,      flushSyncCallbackQueueImpl,    );  } else {    syncQueue.push(callback);  }  return fakeCallbackNode;}

Scheduler中的优先级

说到优先级,咱们来看一下Scheduler本人的优先级级别,它为工作定义了以下几种级别的优先级:

export const NoPriority = 0; // 没有任何优先级export const ImmediatePriority = 1; // 立刻执行的优先级,级别最高export const UserBlockingPriority = 2; // 用户阻塞级别的优先级export const NormalPriority = 3; // 失常的优先级export const LowPriority = 4; // 较低的优先级export const IdlePriority = 5; // 优先级最低,示意工作能够闲置

工作优先级的作用曾经提到过,它是计算工作过期工夫的重要依据,事关过期工作在taskQueue中的排序。

// 不同优先级对应的不同的工作过期工夫距离var IMMEDIATE_PRIORITY_TIMEOUT = -1;var USER_BLOCKING_PRIORITY_TIMEOUT = 250;var NORMAL_PRIORITY_TIMEOUT = 5000;var LOW_PRIORITY_TIMEOUT = 10000;// Never times outvar IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;...// 计算过期工夫(scheduleCallback函数中的内容)var 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;}// startTime可暂且认为是以后工夫var expirationTime = startTime + timeout;

可见,过期工夫是工作开始工夫加上timeout,而这个timeout则是通过工作优先级计算得出。

React中更全面的优先级解说在我写的这一篇文章中:React中的优先级

调度入口 - scheduleCallback

通过下面的梳理,咱们晓得Scheduler中的scheduleCallback是调度流程开始的关键点。在进入这个调度入口之前,咱们先来认识一下Scheduler中的工作是什么模式:

  var newTask = {    id: taskIdCounter++,    // 工作函数    callback,    // 工作优先级    priorityLevel,    // 工作开始的工夫    startTime,    // 工作的过期工夫    expirationTime,    // 在小顶堆队列中排序的根据    sortIndex: -1,  };
  • callback:真正的工作函数,重点,也就是内部传入的工作函数,例如构建fiber树的工作函数:performConcurrentWorkOnRoot
  • priorityLevel:工作优先级,参加计算工作过期工夫
  • startTime:示意工作开始的工夫,影响它在timerQueue中的排序
  • expirationTime:示意工作何时过期,影响它在taskQueue中的排序
  • sortIndex:在小顶堆队列中排序的根据,在辨别好工作是过期或非过期之后,sortIndex会被赋值为expirationTime或startTime,为两个小顶堆的队列(taskQueue,timerQueue)提供排序根据

真正的重点是callback,作为工作函数,它的执行后果会影响到工作实现状态的判断,前面咱们会讲到,临时先无需关注。当初咱们先来看看scheduleCallback做的事件:它负责生成调度工作、依据工作是否过期将工作放入timerQueue或taskQueue,而后触发调度行为,让工作进入调度。残缺代码如下:

function unstable_scheduleCallback(priorityLevel, callback, options) {  // 获取以后工夫,它是计算工作开始工夫、过期工夫和判断工作是否过期的根据  var currentTime = getCurrentTime();  // 确定工作开始工夫  var startTime;  // 从options中尝试获取delay,也就是推迟时间  if (typeof options === 'object' && options !== null) {    var delay = options.delay;    if (typeof delay === 'number' && delay > 0) {      // 如果有delay,那么工作开始工夫就是以后工夫加上delay      startTime = currentTime + delay;    } else {      // 没有delay,工作开始工夫就是以后工夫,也就是工作须要立即开始      startTime = currentTime;    }  } else {    startTime = currentTime;  }  // 计算timeout  var timeout;  switch (priorityLevel) {    case ImmediatePriority:      timeout = IMMEDIATE_PRIORITY_TIMEOUT; // -1      break;    case UserBlockingPriority:      timeout = USER_BLOCKING_PRIORITY_TIMEOUT; // 250      break;    case IdlePriority:      timeout = IDLE_PRIORITY_TIMEOUT; // 1073741823 ms      break;    case LowPriority:      timeout = LOW_PRIORITY_TIMEOUT; // 10000      break;    case NormalPriority:    default:      timeout = NORMAL_PRIORITY_TIMEOUT; // 5000      break;  }  // 计算工作的过期工夫,工作开始工夫 + timeout  // 若是立刻执行的优先级(ImmediatePriority),  // 它的过期工夫是startTime - 1,意味着立即就过期  var expirationTime = startTime + timeout;  // 创立调度工作  var newTask = {    id: taskIdCounter++,    // 真正的工作函数,重点    callback,    // 工作优先级    priorityLevel,    // 工作开始的工夫,示意工作何时能力执行    startTime,    // 工作的过期工夫    expirationTime,    // 在小顶堆队列中排序的根据    sortIndex: -1,  };  // 上面的if...else判断各自分支的含意是:  // 如果工作未过期,则将 newTask 放入timerQueue, 调用requestHostTimeout,  // 目标是在timerQueue中排在最后面的工作的开始工夫的工夫点查看工作是否过期,  // 过期则立即将工作退出taskQueue,开始调度  // 如果工作已过期,则将 newTask 放入taskQueue,调用requestHostCallback,  // 开始调度执行taskQueue中的工作  if (startTime > currentTime) {    // 工作未过期,以开始工夫作为timerQueue排序的根据    newTask.sortIndex = startTime;    push(timerQueue, newTask);    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {      // 如果当初taskQueue中没有工作,并且以后的工作是timerQueue中排名最靠前的那一个      // 那么须要查看timerQueue中有没有须要放到taskQueue中的工作,这一步通过调用      // requestHostTimeout实现      if (isHostTimeoutScheduled) {        // 因为行将调度一个requestHostTimeout,所以如果之前曾经调度了,那么勾销掉        cancelHostTimeout();      } else {        isHostTimeoutScheduled = true;      }      // 调用requestHostTimeout实现工作的转移,开启调度      requestHostTimeout(handleTimeout, startTime - currentTime);    }  } else {    // 工作曾经过期,以过期工夫作为taskQueue排序的根据    newTask.sortIndex = expirationTime;    push(taskQueue, newTask);    // 开始执行工作,应用flushWork去执行taskQueue    if (!isHostCallbackScheduled && !isPerformingWork) {      isHostCallbackScheduled = true;      requestHostCallback(flushWork);    }  }  return newTask;}

这个过程中的重点是工作过期与否的解决。

针对未过期工作,会放入timerQueue,并依照开始工夫排列,而后调用requestHostTimeout,为的是等一会,等到了timerQueue中那个应该最早开始的工作(排在第一个的工作)的开始工夫,再去查看它是否过期,如果它过期则放到taskQueue中,这样工作就能够被执行了,否则持续等。这个过程通过handleTimeout实现。

handleTimeout的职责是:

  • 调用advanceTimers,查看timerQueue队列中过期的工作,放到taskQueue中。
  • 查看是否曾经开始调度,如尚未调度,查看taskQueue中是否曾经有工作:

    • 如果有,而且当初是闲暇的,阐明之前的advanceTimers曾经将过期工作放到了taskQueue,那么当初立刻开始调度,执行工作
    • 如果没有,而且当初是闲暇的,阐明之前的advanceTimers并没有查看到timerQueue中有过期工作,那么再次调用requestHostTimeout反复这一过程。

总之,要把timerQueue中的工作全副都转移到taskQueue中执行掉才行。

针对已过期工作,在将它放入taskQueue之后,调用requestHostCallback,让调度者调度一个执行者去执行工作,也就意味着调度流程开始。

开始调度-找出调度者和执行者

Scheduler通过调用requestHostCallback让工作进入调度流程,回顾下面scheduleCallback最终调用requestHostCallback执行工作的中央:

if (!isHostCallbackScheduled && !isPerformingWork) {  isHostCallbackScheduled = true;  // 开始进行调度  requestHostCallback(flushWork);}

它既然把flushWork作为入参,那么工作的执行者实质上调用的就是flushWork,咱们先不论执行者是如何执行工作的,先关注它是如何被调度的,须要先找出调度者,这须要看一下requestHostCallback的实现:

Scheduler辨别了浏览器环境和非浏览器环境,为requestHostCallback做了两套不同的实现。在非浏览器环境下,应用setTimeout实现.

  requestHostCallback = function(cb) {    if (_callback !== null) {      setTimeout(requestHostCallback, 0, cb);    } else {      _callback = cb;      setTimeout(_flushCallback, 0);    }  };

在浏览器环境,用MessageChannel实现,对于MessageChannel的介绍就不再赘述。

  const channel = new MessageChannel();  const port = channel.port2;  channel.port1.onmessage = performWorkUntilDeadline;  requestHostCallback = function(callback) {    scheduledHostCallback = callback;    if (!isMessageLoopRunning) {      isMessageLoopRunning = true;      port.postMessage(null);    }  };

之所以有两种实现,是因为非浏览器环境不存在屏幕刷新率,没有帧的概念,也就不会有工夫片,这与在浏览器环境下执行工作有本质区别,因为非浏览器环境根本不胡有用户交互,所以该场景下不判断工作执行工夫是否超出了工夫片限度,而浏览器环境工作的执行会有工夫片的限度。除了这一点之外,尽管两种环境下实现形式不一样,然而做的事件大致相同。

先看非浏览器环境,它将入参(执行工作的函数)存储到外部的变量_callback上,而后调度_flushCallback去执行这个此变量_callback,taskQueue被清空。

再看浏览器环境,它将入参(执行工作的函数)存到外部的变量scheduledHostCallback上,而后通过MessageChannel的port去发送一个音讯,让channel.port1的监听函数performWorkUntilDeadline得以执行。performWorkUntilDeadline外部会执行掉scheduledHostCallback,最初taskQueue被清空。

通过下面的形容,能够很分明得找出调度者:非浏览器环境是setTimeout,浏览器环境是port.postMessage。而两个环境的执行者也不言而喻,前者是_flushCallback,后者是performWorkUntilDeadline,执行者做的事件都是去调用理论的工作执行函数。

因为本文围绕Scheduler的工夫片调度行为开展,所以次要探讨浏览器环境下的调度行为,performWorkUntilDeadline波及到调用工作执行函数去执行工作,这个过程中会波及工作的中断和复原工作实现状态的判断,接下来的内容将重点对这两点进行解说。

工作执行 - 从performWorkUntilDeadline说起

在文章结尾的原理概述中提到过performWorkUntilDeadline作为执行者,它的作用是依照工夫片的限度去中断工作,并告诉调度者再次调度一个新的执行者去持续工作。依照这种认知去看它的实现,会很清晰。

  const performWorkUntilDeadline = () => {    if (scheduledHostCallback !== null) {      // 获取以后工夫      const currentTime = getCurrentTime();      // 计算deadline,deadline会参加到      // shouldYieldToHost(依据工夫片去限度工作执行)的计算中      deadline = currentTime + yieldInterval;      // hasTimeRemaining示意工作是否还有剩余时间,      // 它和工夫片一起限度工作的执行。如果没有工夫,      // 或者工作的执行工夫超出工夫片限度了,那么中断工作。      // 它的默认为true,示意始终有剩余时间      // 因为MessageChannel的port在postMessage,      // 是比setTimeout还靠前执行的宏工作,这意味着      // 在这一帧开始时,总是会有剩余时间      // 所以当初中断工作只看工夫片的了      const hasTimeRemaining = true;      try {        // scheduledHostCallback去执行工作的函数,        // 当工作因为工夫片被打断时,它会返回true,示意        // 还有工作,所以会再让调度者调度一个执行者        // 继续执行工作        const hasMoreWork = scheduledHostCallback(          hasTimeRemaining,          currentTime,        );        if (!hasMoreWork) {          // 如果没有工作了,进行调度          isMessageLoopRunning = false;          scheduledHostCallback = null;        } else {          // 如果还有工作,持续让调度者调度执行者,便于持续          // 实现工作          port.postMessage(null);        }      } catch (error) {        port.postMessage(null);        throw error;      }    } else {      isMessageLoopRunning = false;    }    needsPaint = false;  };

performWorkUntilDeadline外部调用的是scheduledHostCallback,它早在开始调度的时候就被requestHostCallback赋值为了flushWork,具体能够翻到下面回顾一下requestHostCallback的实现。

flushWork作为真正去执行工作的函数,它会循环taskQueue,逐个调用外面的工作函数。咱们看一下flushWork具体做了什么。

function flushWork(hasTimeRemaining, initialTime) {  ...  return workLoop(hasTimeRemaining, initialTime);  ...}

它调用了workLoop,并将其调用的后果return了进来。那么当初工作执行的核心内容看来就在workLoop中了。workLoop的调用使得工作最终被执行。

工作中断和复原

要了解workLoop,须要回顾Scheduler的性能之一:通过工夫片限度工作的执行工夫。那么既然工作的执行被限度了,它必定有可能是尚未实现的,如果未实现被中断,那么须要将它复原。

所以工夫片下的工作执行具备上面的重要特点:会被中断,也会被复原。

不难揣测出,workLoop作为理论执行工作的函数,它做的事件必定与工作的中断复原无关。咱们先看一下它的构造:

function workLoop(hasTimeRemaining, initialTime) {  // 获取taskQueue中排在最后面的工作  currentTask = peek(taskQueue);  while (currentTask !== null) {    if (currentTask.expirationTime > currentTime &&     (!hasTimeRemaining || shouldYieldToHost())) {       // break掉while循环       break    }    ...    // 执行工作    ...    // 工作执行结束,从队列中删除    pop(taskQueue);    // 获取下一个工作,持续循环    currentTask = peek(taskQueue);  }  if (currentTask !== null) {    // 如果currentTask不为空,阐明是工夫片的限度导致了工作中断    // return 一个 true通知内部,此时工作还未执行完,还有工作,    // 翻译成英文就是hasMoreWork    return true;  } else {    // 如果currentTask为空,阐明taskQueue队列中的工作曾经都    // 执行完了,而后从timerQueue中找工作,调用requestHostTimeout    // 去把task放到taskQueue中,到时会再次发动调度,然而这次,    // 会先return false,通知内部以后的taskQueue曾经清空,    // 先进行执行工作,也就是终止任务调度    const firstTimer = peek(timerQueue);    if (firstTimer !== null) {      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);    }    return false;  }}

workLoop中能够分为两大部分:循环taskQueue执行工作 和 工作状态的判断。

循环taskQueue执行工作

暂且不论工作如何执行,只关注工作如何被工夫片限度,workLoop中:

if (currentTask.expirationTime > currentTime &&     (!hasTimeRemaining || shouldYieldToHost())) {   // break掉while循环   break}

currentTask就是以后正在执行的工作,它停止的判断条件是:工作并未过期,但曾经没有剩余时间了(因为hasTimeRemaining始终为true,这与MessageChannel作为宏工作的执行机会无关,咱们疏忽这个判断条件,只看工夫片),或者应该让出执行权给主线程(工夫片的限度),也就是说currentTask执行得好好的,可是工夫不容许,那只能先break掉本次while循环,使得本次循环上面currentTask执行的逻辑都不能被执行到(此处是中断工作的要害)。然而被break的只是while循环,while下部还是会判断currentTask的状态。

因为它只是被停止了,所以currentTask不可能是null,那么会return一个true通知内部还没完事呢(此处是复原工作的要害),否则阐明全副的工作都曾经执行完了,taskQueue曾经被清空了,return一个false好让内部终止本次调度。而workLoop的执行后果会被flushWork return进来,flushWork实际上是scheduledHostCallback,当performWorkUntilDeadline检测到scheduledHostCallback的返回值(hasMoreWork)为false时,就会进行调度。

回顾performWorkUntilDeadline中的行为,能够很清晰地将工作中断复原的机制串联起来:

  const performWorkUntilDeadline = () => {    ...    const hasTimeRemaining = true;    // scheduledHostCallback去执行工作的函数,    // 当工作因为工夫片被打断时,它会返回true,示意    // 还有工作,所以会再让调度者调度一个执行者    // 继续执行工作    const hasMoreWork = scheduledHostCallback(      hasTimeRemaining,      currentTime,    );    if (!hasMoreWork) {      // 如果没有工作了,进行调度      isMessageLoopRunning = false;      scheduledHostCallback = null;    } else {      // 如果还有工作,持续让调度者调度执行者,便于持续      // 实现工作      port.postMessage(null);    }  };

当工作被打断之后,performWorkUntilDeadline会再让调度者调用一个执行者,继续执行这个工作,直到工作实现。然而这里有一个重点是如何判断该工作是否实现呢?这就须要钻研workLoop中执行工作的那局部逻辑。

判断单个工作的实现状态

工作的中断复原是一个反复的过程,该过程会始终反复到工作实现。所以判断工作是否实现十分重要,而工作未实现则会反复执行工作函数

咱们能够用递归函数做类比,如果没到递归边界,就反复调用本人。这个递归边界,就是工作实现的标记。因为递归函数所解决的工作就是它自身,能够很不便地把工作实现作为递归边界去结束任务,然而Scheduler中的workLoop与递归不同的是,它只是一个执行工作的,这个工作并不是它本人产生的,而是内部的(比方它去执行React的工作循环渲染fiber树),它能够做到反复执行工作函数,但边界(即工作是否实现)却无奈像递归那样间接获取,只能依赖工作函数的返回值去判断。即:若工作函数返回值为函数,那么就阐明当前任务尚未实现,须要持续调用工作函数,否则工作实现workLoop就是通过这样的方法判断单个工作的实现状态

在真正解说workLoop中的执行工作的逻辑之前,咱们用一个例子来了解一下判断工作实现状态的外围。

有一个工作calculate,负责把currentResult每次加1,始终到3为止。当没到3的时候,calculate不是去调用它本身,而是将本身return进来,一旦到了3,return的是null。这样内部才能够晓得calculate是否曾经实现了工作。

const result = 3let currentResult = 0function calculate() {    currentResult++    if (currentResult < result) {        return calculate    }    return null}

下面是工作,接下来咱们模仿一下调度,去执行calculate。但执行应该是基于工夫片的,为了察看成果,只用setInterval去模仿因为工夫片停止复原工作的机制(相当毛糙的模仿,只需明确这是工夫片的模仿即可,重点关注工作实现状态的判断),1秒执行它一次,即一次只实现全副工作的三分之一。

另外Scheduler中有两个队列去治理工作,咱们暂且只用一个队列(taskQueue)存储工作。除此之外还须要三个角色:把工作退出调度的函数(调度入口scheduleCallback)、开始调度的函数(requestHostCallback)、执行工作的函数(workLoop,要害逻辑所在)。

const result = 3let currentResult = 0function calculate() {    currentResult++    if (currentResult < result) {        return calculate    }    return null}// 寄存工作的队列const taskQueue = []// 寄存模仿工夫片的定时器let interval// 调度入口----------------------------------------const scheduleCallback = (task, priority) => {    // 创立一个专属于调度器的工作    const taskItem = {        callback: task,        priority    }    // 向队列中增加工作    taskQueue.push(taskItem)    // 优先级影响到工作在队列中的排序,将优先级最高的工作排在最后面    taskQueue.sort((a, b) => (a.priority - b.priority))    // 开始执行工作,调度开始    requestHostCallback(workLoop)}// 开始调度-----------------------------------------const requestHostCallback = cb => {    interval = setInterval(cb, 1000)}// 执行工作-----------------------------------------const workLoop = () => {    // 从队列中取出工作    const currentTask = taskQueue[0]    // 获取真正的工作函数,即calculate    const taskCallback = currentTask.callback    // 判断工作函数否是函数,若是,执行它,将返回值更新到currentTask的callback中    // 所以,taskCallback是上一阶段执行的返回值,若它是函数类型,则阐明上一次执行返回了函数    // 类型,阐明工作尚未实现,本次继续执行这个函数,否则阐明工作实现。    if (typeof taskCallback === 'function') {        currentTask.callback = taskCallback()        console.log('正在执行工作,以后的currentResult 是', currentResult);    } else {        // 工作实现。将以后的这个工作从taskQueue中移除,并革除定时器        console.log('工作实现,最终的 currentResult 是', currentResult);        taskQueue.shift()        clearInterval(interval)    }}// 把calculate退出调度,也就意味着调度开始scheduleCallback(calculate, 1)

最终的执行后果如下:

正在执行工作,以后的currentResult 是 1正在执行工作,以后的currentResult 是 2正在执行工作,以后的currentResult 是 3工作实现,最终的 currentResult 是 3

可见,如果没有加到3,那么calculate会return它本人,workLoop若判断返回值为function,阐明工作还未实现,它就会持续调用工作函数去实现工作

这个例子只保留了workLoop中判断工作实现状态的逻辑,其余的中央并不欠缺,要以真正的的workLoop为准,当初让咱们贴出它的全副代码,残缺地看一下真正的实现:

function workLoop(hasTimeRemaining, initialTime) {  let currentTime = initialTime;  // 开始执行前检查一下timerQueue中的过期工作,  // 放到taskQueue中  advanceTimers(currentTime);  // 获取taskQueue中最紧急的工作  currentTask = peek(taskQueue);  // 循环taskQueue,执行工作  while (    currentTask !== null &&    !(enableSchedulerDebugging && isSchedulerPaused)  ) {    if (      currentTask.expirationTime > currentTime &&      (!hasTimeRemaining || shouldYieldToHost())    ) {      // 工夫片的限度,中断工作      break;    }    // 执行工作 ---------------------------------------------------    // 获取工作的执行函数,这个callback就是React传给Scheduler    // 的工作。例如:performConcurrentWorkOnRoot    const callback = currentTask.callback;    if (typeof callback === 'function') {      // 如果执行函数为function,阐明还有工作可做,调用它      currentTask.callback = null;      // 获取工作的优先级      currentPriorityLevel = currentTask.priorityLevel;      // 工作是否过期      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;      // 获取工作函数的执行后果      const continuationCallback = callback(didUserCallbackTimeout);      if (typeof continuationCallback === 'function') {        // 查看callback的执行后果返回的是不是函数,如果返回的是函数,则将这个函数作为当前任务新的回调。        // concurrent模式下,callback是performConcurrentWorkOnRoot,其外部依据以后调度的工作        // 是否雷同,来决定是否返回本身,如果雷同,则阐明还有工作没做完,返回本身,其作为新的callback        // 被放到以后的task上。while循环实现一次之后,查看shouldYieldToHost,如果须要让出执行权,        // 则中断循环,走到下方,判断currentTask不为null,返回true,阐明还有工作,回到performWorkUntilDeadline        // 中,判断还有工作,持续port.postMessage(null),调用监听函数performWorkUntilDeadline(执行者),        // 持续调用workLoop行工作        // 将返回值持续赋值给currentTask.callback,为得是下一次可能继续执行callback,        // 获取它的返回值,持续判断工作是否实现。        currentTask.callback = continuationCallback;      } else {        if (currentTask === peek(taskQueue)) {          pop(taskQueue);        }      }      advanceTimers(currentTime);    } else {      pop(taskQueue);    }    // 从taskQueue中持续获取工作,如果上一个工作未实现,那么它将不会    // 被从队列剔除,所以获取到的currentTask还是上一个工作,会持续    // 去执行它    currentTask = peek(taskQueue);  }  // return 的后果会作为 performWorkUntilDeadline  // 中判断是否还须要再次发动调度的根据  if (currentTask !== null) {    return true;  } else {    // 若工作实现,去timerQueue中找须要最早开始执行的那个工作    // 调度requestHostTimeout,目标是等到了它的开始事件时把它    // 放到taskQueue中,再次调度    const firstTimer = peek(timerQueue);    if (firstTimer !== null) {      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);    }    return false;  }}

所以,workLoop是通过判断工作函数的返回值去辨认工作的实现状态的

总结一下判断工作实现状态与工作执行的整体关系:当开始调度后,调度者调度执行者去执行工作,实际上是执行工作上的callback(也就是工作函数)。如果执行者判断callback返回值为一个function,阐明未实现,那么会将返回的这个function再次赋值给工作的callback,因为工作还未实现,所以并不会被剔除出taskQueue,currentTask获取到的还是它,while循环到下一次还是会继续执行这个工作,直到工作实现出队,才会持续下一个。

另外有一个点须要提一下,就是构建fiber树的工作函数:performConcurrentWorkOnRoot,它承受的参数是fiberRoot。

function performConcurrentWorkOnRoot(root) {  ...}

在workLoop中它会被这样调用(callback即为performConcurrentWorkOnRoot):

const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;const continuationCallback = callback(didUserCallbackTimeout);

didUserCallbackTimeout显著是boolean类型的值,并不是fiberRoot,但performConcurrentWorkOnRoot却能失常调用。这是因为在开始调度,以及后续的return本身的时候,都在bind的时候将root传进去了。

// 调度的时候scheduleCallback(  schedulerPriorityLevel,  performConcurrentWorkOnRoot.bind(null, root),);// 其外部return本身的时候function performConcurrentWorkOnRoot(root) {  ...  if (root.callbackNode === originalCallbackNode) {    return performConcurrentWorkOnRoot.bind(null, root);  }  return null;}

这样的话,再给它传参数调用它,那这个参数只能作为后续的参数被接管到,performConcurrentWorkOnRoot中接管到的第一个参数还是bind时传入的那个root,这个特点与bind的实现无关。能够跑一下上面的这个简略例子:

function test(root, b) {    console.log(root, b)}function runTest() {    return test.bind(null, 'root')}runTest()(false)// 后果:root false

以上,是Scheduler执行工作时的两大外围逻辑:工作的中断与复原 & 工作实现状态的判断。它们协同单干,若工作未实现就中断了工作,那么调度的新执行者会复原执行该工作,直到它实现。到此,Scheduler的外围局部曾经写完了,上面是勾销调度的逻辑。

勾销调度

通过下面的内容咱们晓得,工作执行实际上是执行的工作的callback,当callback是function的时候去执行它,当它为null的时候会产生什么?以后的工作会被剔除出taskQueue,让咱们再来看一下workLoop函数:

function workLoop(hasTimeRemaining, initialTime) {  ...  // 获取taskQueue中最紧急的工作  currentTask = peek(taskQueue);  while (currentTask !== null) {    ...    const callback = currentTask.callback;    if (typeof callback === 'function') {      // 执行工作    } else {      // 如果callback为null,将工作出队      pop(taskQueue);    }    currentTask = peek(taskQueue);  }  ...}

所以勾销调度的要害就是将以后这个工作的callback设置为null。

function unstable_cancelCallback(task) {  ...  task.callback = null;}

为什么设置callback为null就能勾销任务调度呢?因为在workLoop中,如果callback是null会被移出taskQueue,所以以后的这个工作就不会再被执行了。它勾销的是当前任务的执行,while循环还会继续执行下一个工作。

勾销工作在React的场景是什么呢?当一个更新工作正在进行的时候,忽然有高优先级工作进来了,那么就要勾销掉这个正在进行的工作,这只是泛滥场景中的一种。

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {  ...  if (existingCallbackNode !== null) {    const existingCallbackPriority = root.callbackPriority;    if (existingCallbackPriority === newCallbackPriority) {      return;    }    // 勾销掉原有的工作    cancelCallback(existingCallbackNode);  }  ...}

总结

Scheduler用工作优先级去实现多任务的治理,优先解决高优工作,用工作的继续调度来解决工夫片造成的单个工作中断复原问题。工作函数的执行后果为是否应该完结当前任务的调度提供参考,另外,在无限的工夫片内实现工作的一部分,也为浏览器响应交互与实现工作提供了保障。

欢送扫码关注公众号,发现更多技术文章