React Scheduler 任务调度平民分析

1,397 阅读12分钟

前言

本文内容基于 React 16.8.6 版本,仅作为记录一些个人阅读源码的分享与体会

React 的任务调度机制其实是对 requestIdleCallback 的实现,至于 requestIdleCallback 是什么,可以自行查阅资料或者阅读本文后面会进行一些粗略的介绍。

React 自行实现任务调度而不直接使用 requestIdleCallback 的原因是:存在兼容性问题,如下图:

image

基本上很多游览器或者版本都无法支持。

还有一点缺点就是:requestIdleCallback 只能一秒回调 20 次。

requestIdleCallback is called only 20 times per second - Chrome on my 6x2 core Linux machine, it's not really useful for UI work.

为此 React 自己实现了一套任务调度机制,那任务调度需要满足什么要求呢?

核心就在于:如何多次在浏览器空闲时且是渲染后才调用回调方法?

  1. 首先能够 多次 回调,一般来说想到的就是定时器了,而在多种定时器中,requestAnimationFrame 是能够在游览器一帧实行渲染前执行的,在这时候处理任务有利于一帧渲染后空闲时间执行任务。

  2. 第二,如何在 渲染后空闲时间才执行任务,那就是使用 MessageChannel,那为什么是 MessageChannel,这里涉及到一个知识点:“宏任务”和“微任务”,而 MessageChannel 就是一种宏任务,宏任务会在主栈任务执行完成后去 Event loop 出栈一个宏任务进行处理的机制,该知识点的详细介绍可以查看本人之前的文章(链接)

下面先简单看看上诉的几个知识点 ~

调度核心知识点

requestAnimationFrame

requestAnimationFrame 在每一帧的渲染开始之前阶段执行,一般用来进行复杂动画的绘制。该函数接受一个接收 DOMHighResTimeStamp 参数的 callback 函数作为参数,返回一个 requestId 供 cancelAnimationFrame 以取消。

image

上图为游览器每一帧做的事情,我们可以看到 requestAnimationFrame 是在 rAF 阶段被调用。

现在可以先看看 requestAnimationFrame 实现一个简易的时间分片调度实现:

// create 1000 tasks 
const tasks = Array.from({ length: 1000 }, () => () => { console.log('task run'); })

const doTasks = (fromIndex = 0) => {
	const start = Date.now();
	let i = fromIndex;
	let end;
	
	do {
		tasks[i++](); // do task
		end = Date.now();
	} while(i < tasks.length && end - start < 20); // 就算有时间也只给单次执行 20 毫秒
	
	console.log('tasks remain: ', 1000 - i);
	// if remaining tasks exsis when timeout. Run at next frame
	if (i < tasks.length) {
		requestAnimationFrame(doTasks.bind(null, i));
	}
}

// start tasks scheduler
requestAnimationFrame(doTasks.bind(null, 0))

/** 
output:
	168 task run
	tasks remain:  832
	178 task run
	asks remain:  654
	162 task run
	tasks remain:  492
	119 task run
	tasks remain:  373
	158 task run
	tasks remain:  215
	87 task run
	tasks remain:  128
	125 task run
	tasks remain:  3
	3 task run
	tasks remain:  0
*/

从上诉例子中可以看到,使用 requestAnimationFrame 进行了简单的在每一帧中的 20ms内处理任务。这就做到了“多次”循环定时器调用的作用,现在有一个问题,这里的 20ms 是如何定义的,实际上在 react 的调度任务中,是不会定义死这 20ms 的,它会根据游览器空闲时间和不同设备游览器的刷新率,也就是一帧多少时长好计算的。

requestIdleCallback

与每帧执行的 requestAnimationFrame 相比,requestIdleCallback 是一个低优先级调度,当且仅当浏览器空闲时才会执行任务的调度。

requestIdleCallback Api

window.requestIdleCallback(
  callback: (dealine: IdleDeadline) => void,
  option?: {timeout: number} // 超时时间,防止一直不执行 callback 的一种优先级设置
)

如果游览器有可执行时间分配的时候,就会调用 callback 方法,并传入 IdleDeadline 对象。

IdleDeadline 的接口如下:

interface IdleDealine {
  didTimeout: boolean // 表示任务执行是否超过约定时间
  timeRemaining(): DOMHighResTimeStamp // 任务可供执行的剩余时间
}

requestIdleCallback 的意思是让浏览器在'有空'的时候就执行我们的回调,这个回调会传入一个期限,表示浏览器有多少时间供我们执行,在这个时间范围内执行。

下面看个例子:

const tasks = Array.from({ length: 1000 }, () => () => { console.log('task run'); })
const doTasks = (fromIndex = 0, idleDeadline) => {
	let i = fromIndex;
	let end;
	
	console.log('time remains: ', idleDeadline.timeRemaining());
	do {
		tasks[i++](); // do task
	} while(i < tasks.length && idleDeadline.timeRemaining() > 0); // Do tasks in 20ms
	
	console.log('tasks remain: ', 1000 - i);
	// if remaining tasks exsis when timeout. Run at next frame
	if (i < tasks.length) {
		requestIdleCallback(doTasks.bind(null, i));
	}
}

// start tasks scheduler
requestIdleCallback(doTasks.bind(null, 0))

/**
output:
	time remains:  49.970000000000006
	360 task run
	tasks remain:  640
	time remains:  49.77
	395 task run
	tasks remain:  245
	time remains:  29.255000000000003
	215 task run
	tasks remain:  30
	time remains:  49.96000000000001
	30 task run
	tasks remain:  0
*/

这会有一个问题,那就是如果一直未能分配时间给我们呢?这会导致任务被饿死,拿不到资源,可以设置一个超时时间,就是 requestIdleCallback 的第二个参数 option?: {timeout: number} 可以设置。

对于该 API 在 React Fiber 中也是用了该思想,不过的是,React 为了兼容性,它利用 MessageChannel 模拟将回调延迟到'绘制操作'之后执行。

MessageChannel

介绍 MessageChannel 之前需要先了解 JS 的 macroTask 和 microTask:

macroTask 与 microTask 指的就是宏任务微任务,在异步队列中,把异步任务分成了宏任务微任务两种;

macroTask 与 microTask 任务都是异步任务,执行的时候都会被入栈到异步任务队列中,并且等待某个时机被主线程入栈执行;

macroTask

macroTask(宏任务) 在浏览器端,其可以理解为该任务执行完后,在下一个 macroTask 执行开始前,浏览器可以进行页面渲染。

触发 macroTask 任务的操作包括:

  • script(整体代码)

  • setTimeout、setInterval、setImmediate(浏览器完成其他操作(例如事件和显示更新)后立即运行回调函数-链接)

  • I/O、UI交互事件

  • postMessage、MessageChannel

microTask

microTask(微任务)可以理解为在 macroTask 任务执行后,页面渲染前立即执行的任务。

触发 microTask 任务的操作包括:

  • Promise.then

  • MutationObserver(提供了监视对DOM树所做的更改的功能-链接)

  • process.nextTick(Node环境)

使用案例(链接)

var channel = new MessageChannel();
var para = document.querySelector('p');
    
var ifr = document.querySelector('iframe');
var otherWindow = ifr.contentWindow;

ifr.addEventListener("load", iframeLoaded, false);
    
function iframeLoaded() {
  otherWindow.postMessage('Hello from the main page!', '*', [channel.port2]);
}

channel.port1.onmessage = handleMessage;
function handleMessage(e) {
  para.innerHTML = e.data;
}   

从这里可以看出 MessageChannel 属于 macroTask,这样利用 MessageChannel 模拟可将回调延迟到'绘制操作'之后执行了。

Scheduler

阅读了上面关于 React 任务调度的几个核心知识点后,接下来就可以着手阅读了解一下源码具体是如何实现的了。

React 的调度算法的源码位置为:packages/scheduler/src/Scheduler.js

任务优先级

优先级

var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var LowPriority = 4;
var IdlePriority = 5;

时间戳

/**
 * Max 31 bit integer. The max integer size in V8 for 32-bit systems.
 * Math.pow(2, 30) - 1
 * 0b111111111111111111111111111111
 */
var maxSigned31BitInt = 1073741823;

/** Times out immediately */
var IMMEDIATE_PRIORITY_TIMEOUT = -1;

/** Eventually times out */
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;

/** Never times out */
var IDLE_PRIORITY = maxSigned31BitInt;

React 在调度逻辑中,首先定义了 5 个优先级,根据不同的等级匹配上对应的时间戳来区别每一个任务的执行顺序。

也就是说,假设当前时间为 5000 并且分别有两个优先级不同的任务要执行。前者属于 ImmediatePriority,后者属于 UserBlockingPriority,那么两个任务计算出来的时间分别为 4999 和 5250。通过这个时间可以比对大小得出谁的优先级高。

任务优先级排序

调度开始,首先开始对任务进行排序 unstable_scheduleCallback

function unstable_scheduleCallback(callback, deprecated_options) {
// 获取当前时间 getCurrentTime() 内部使用的是 Performance.now()
  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 LowPriority:
        expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
        break;
      case NormalPriority:
      default:
        expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
    }
  }

  // 构建任务节点,后续初始化节点时能够设置对应的上一个节点和下一个节点
  var newNode = {
    callback,
    priorityLevel: currentPriorityLevel,
    expirationTime,
    next: null,
    previous: null,
  };

  // 将新的节点插入列表中,先按到期时间排序
  // 这是一个双向循环队列
  if (firstCallbackNode === null) {
    // 如果第一个节点为 null 表示当前队列中没有节点,也就是只有 newNode
    // 因此自己循环
    firstCallbackNode = newNode.next = newNode.previous = newNode;
    // 发起调度准备
    ensureHostCallbackIsScheduled();
  } else {
    // 意思为:排序的时候,next 存储着 newNode 的下一个节点是谁
    var next = null;
    // node 保存的是当前正在循环查找的节点
    var node = firstCallbackNode;
    do {
      if (node.expirationTime > expirationTime) {
        // 当前循环对比节点 node 的到期时间比当前新节点 newNode 的到期时间长,
        // 所以 next 存放 node,表示这个节点将在 newNode 之后
        next = node;
        break;
      }
      // 循环下一个对比节点
      node = node.next;
    } while (node !== firstCallbackNode); // 直到循环对比结束

    if (next === null) {
      // 如果 next 还是为 null,表示找不到优先级比 newNode 低的,那 next  存放第一个,因为是循环队列,后续就会把 newNode 下一个节点指向 next
      // 注意的是 expirationTime 越大,优先级越低
      next = firstCallbackNode;
    } else if (next === firstCallbackNode) {
      // 这种情况就是 newNode 是优先级最高的,也就是 expirationTime 最小,next 存放的是 firstCallbackNode 也就是队列第一个节点
      // 之后 next 就作为 newNode 的下一个节点了,也就变成第二个节点
      // firstCallbackNode 设置为 newNode 为第一个节点
      firstCallbackNode = newNode;

      ensureHostCallbackIsScheduled(); // 触发调度准备
    }

    // 对双向队列进行 newNode 插入
    var previous = next.previous;
    previous.next = next.previous = newNode;
    newNode.next = next;
    newNode.previous = previous;
  }

  return newNode;
}

上述代码就是源代码的 unstable_scheduleCallback 方法,这里已经做上注释,可能很好的理解,这一个函数作用就是对任务进行优先级评估和排序的功能

调度前准备

经过 unstable_scheduleCallback 的排序,接下来进行一个是否能够进行调度的判断

ensureHostCallbackIsScheduled

function ensureHostCallbackIsScheduled() {
  // 有调度任务在执行中,直接退出,以防止重新进入
  if (isExecutingCallback) {
    // Don't schedule work yet; wait until the next time we yield.
    return;
  }
  // Schedule the host callback using the earliest expiration in the list.
  var expirationTime = firstCallbackNode.expirationTime;
  // 判断是否有正在调度,但是却还没轮到执行的任务
  if (!isHostCallbackScheduled) {
    // 如果没有,则设置为,true,当前任务进行调度
    isHostCallbackScheduled = true;
  } else {
    // Cancel the existing host callback.
    // 如果有,则中断处理,并且让新进来的 newNode 任务进行调度,
    // 因为 newNode 优先级更高
    cancelHostCallback();
  }
  requestHostCallback(flushWork, expirationTime);
}

这里会先判断 isExecutingCallback 这个变量会在任务执行函数 flushWork 中进行变更。

isHostCallbackScheduled 在任务准备进入调度的时候会进行设置为 ture,在 flushWork 中当 firstCallbackNode 为 null 的时候,也就是执行完当前调度任务的时候,会设置为 false

cancelHostCallback

取消现有的或者待处理的任务回调

cancelHostCallback = function() {
    scheduledHostCallback = null; // 取消当前任务调度
    isMessageEventScheduled = false; // 标识可以进行 postMessage ,也就是未有任务正在执行
    timeoutTime = -1; // 初始化
};

requestHostCallback

在经过 ensureHostCallbackIsScheduled 调度的判断,在 requestHostCallback 还会进行以下判断:

  1. 对当前任务的时间 timeoutTime(是否到时间了) 和任务的 isFlushingHostCallback (是否已经完成上一个调度正在执行任务了)。
  2. 对任务未超时且还没有开启一个调度任务 isAnimationFrameScheduled 进行判断

经过 1 判断,将不要等待下一帧。在新事件中,请继续尽快工作。

经过 2 判断,触发 requestAnimationFrameWithTimeout 开启定时器调度

 requestHostCallback = function(callback, absoluteTimeout) {
    scheduledHostCallback = callback;
    timeoutTime = absoluteTimeout; // 这个就是任务的到期时间
    if (isFlushingHostCallback || absoluteTimeout < 0) {
        // absoluteTimeout 过期时间已经小于零,isFlushingHostCallback已经完成调度正在执行任务了
        // Don't wait for the next frame. Continue working ASAP, in a new event.
        port.postMessage(undefined);
    } else if (!isAnimationFrameScheduled) {
        // If rAF didn't already schedule one, we need to schedule a frame.
        // TODO: If this rAF doesn't materialize because the browser throttles, we
        // might want to still have setTimeout trigger rIC as a backup to ensure
        // that we keep performing work.
        // isAnimationFrameScheduled 为 false 该任务未超时且没开启一个调度任务
        isAnimationFrameScheduled = true;
        requestAnimationFrameWithTimeout(animationTick);
    }
};

定时器

上面我们说到,要模拟实现 requestIdleCallback 机制,那就需要一个定时器,这里的 requestAnimationFrameWithTimeout 就是进行定时器的安排;

还有就是,在执行 requestAnimationFrameWithTimeout 方法的时候,会传入一个 animationTick,这个是调度算法的核心,后面一节会解释到;

接下来直接来看代码

var localSetTimeout = typeof setTimeout === 'function' ? setTimeout : undefined;
var localClearTimeout =
  typeof clearTimeout === 'function' ? clearTimeout : undefined;
  
var localRequestAnimationFrame =
  typeof requestAnimationFrame === 'function'
    ? requestAnimationFrame
    : undefined;
var localCancelAnimationFrame =
  typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;
  
var requestAnimationFrameWithTimeout = function(callback) {
  // schedule rAF and also a setTimeout
  rAFID = localRequestAnimationFrame(function(timestamp) {
    // cancel the setTimeout
    localClearTimeout(rAFTimeoutID);
    callback(timestamp);
  });
  rAFTimeoutID = localSetTimeout(function() {
    // cancel the requestAnimationFrame
    localCancelAnimationFrame(rAFID);
    callback(getCurrentTime());
  }, ANIMATION_FRAME_TIMEOUT);
};

从源码的这个 requestAnimationFrameWithTimeout 方法中我们可以看到,在定时器的处理上,使用了两种方案并用的方式,包括了 requestAnimationFramesetTimeout,那问题来了,为什么这里会要用到 setTimeout 呢?

这是因为 requestAnimationFrame 在系统进入后台的时候,会失去作用,这个使用就采用了 setTimeout 进行一个弥补的措施。

requestAnimationFrameWithTimeout 定时器的作用下,会进行多次的 animationTick 回调,该回调进行下一帧时长(nextFrameTime)与下一帧结束时间(frameDeadline)的计算,并判断是否进行任务安排进入

调度核心算法

根据设备刷新率计算下一帧时长(nextFrameTime)与下一帧结束时间(frameDeadline)

var animationTick = function(rafTime) {
    // 调度程序队列在帧末尾不为空,则它
    // 将继续在该回调内部刷新。
    // 如果队列为空,将立即退出。
    
    if (scheduledHostCallback !== null) {
      // Eagerly schedule the next animation callback at the beginning of the
      // frame. If the scheduler queue is not empty at the end of the frame, it
      // will continue flushing inside that callback. If the queue *is* empty,
      // then it will exit immediately. Posting the callback at the start of the
      // frame ensures it's fired within the earliest possible frame. If we
      // waited until the end of the frame to post the callback, we risk the
      // browser skipping a frame and not firing the callback until the frame
      // after that.
      requestAnimationFrameWithTimeout(animationTick);
    } else {
      // No pending work. Exit.
      isAnimationFrameScheduled = false;
      return;
    }

    /**
     * 这个 nextFrameTime 有两个含义,
     * 一开始的时候,nextFrameTime = rafTime(调用时间) - frameDeadline(计算出来的下一帧调用该方法的时间,一开始为0) + activeFrameTime(一帧的时长默认为:33),
     * 这个时候 nextFrameTime == frameDeadline == previousFrameTime,nextFrameTime 就是估算得到的下一次执行时间
     * 第二次调用的时候,不管 rafTime 会在 frameDeadline 之前或者之后调用,
     * 通过计算公式 rafTime - frameDeadline + activeFrameTime,就能够计算出来 nextFrameTime(下一帧的时长)
     * 这时候 previousFrameTime 的意义也变成了,上一帧的时长了,
     * 如果前后两帧的时长都小于 activeFrameTime(当前保存的帧时长)的话,就进行更新 activeFrameTime,
     * 更新 activeFrameTime 的时长是取 previousFrameTime 和 nextFrameTime 两个较长的那个
     */
    var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
    if (
      nextFrameTime < activeFrameTime &&
      previousFrameTime < activeFrameTime
    ) {
      if (nextFrameTime < 8) {
        // Defensive coding. We don't support higher frame rates than 120hz.
        // If the calculated frame time gets lower than 8, it is probably a bug.
        nextFrameTime = 8;
      }
      // If one frame goes long, then the next one can be short to catch up.
      // If two frames are short in a row, then that's an indication that we
      // actually have a higher frame rate than what we're currently optimizing.
      // We adjust our heuristic dynamically accordingly. For example, if we're
      // running on 120hz display or 90hz VR display.
      // Take the max of the two in case one of them was an anomaly due to
      // missed frame deadlines.
      activeFrameTime =
        nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
    } else {
      previousFrameTime = nextFrameTime;
    }
    frameDeadline = rafTime + activeFrameTime;
    if (!isMessageEventScheduled) {
      isMessageEventScheduled = true;
      port.postMessage(undefined);
    }
  };

在通过一些变量计算后,最后会触发 port.postMessage 宏任务,这个宏任务就保证了,在游览器每一帧的渲染结束后,再去执行这个宏任务的机制。

MessageChannel 安排任务入队列

通过 animationTick 计算出 frameDeadline 后,会在 postMessage 中去使用,

  channel.port1.onmessage = function(event) {
    isMessageEventScheduled = false; // 接触 postMessage 消息发送控制

    // 将当前调度任务保存
    var prevScheduledCallback = scheduledHostCallback;
    var prevTimeoutTime = timeoutTime;
    scheduledHostCallback = null;
    timeoutTime = -1;

    var currentTime = getCurrentTime();

    var didTimeout = false;
    // 当前队列中 onmessage 宏任务执行的时候,在下一帧调用时间之后了
    // 说明上一帧游览器渲染后,没有空闲剩余时间去执行
    if (frameDeadline - currentTime <= 0) {
      // There's no time left in this idle period. Check if the callback has
      // a timeout and whether it's been exceeded.
      if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
        // 当前一个任务的过期时间存在,并且当前时间已经大于这个过期时间了
        // Exceeded the timeout. Invoke the callback even though there's no
        // time left.
        didTimeout = true;
      } else {
        // No timeout.
        if (!isAnimationFrameScheduled) {
          // 没时间且没任务未超时,重新调度
          // Schedule another animation callback so we retry later.
          isAnimationFrameScheduled = true;
          requestAnimationFrameWithTimeout(animationTick);
        }
        // 退出本次调度而不调用回调。
        // Exit without invoking the callback.
        scheduledHostCallback = prevScheduledCallback;
        timeoutTime = prevTimeoutTime;
        return;
      }
    }

    if (prevScheduledCallback !== null) {
      isFlushingHostCallback = true;
      try {
      // 执行队列任务,传入 didTimeout 判断是否超时
        prevScheduledCallback(didTimeout);
      } finally {
        isFlushingHostCallback = false;
      }
    }
  };

上面的 onmessage 执行的逻辑,都进行了相关的注释说明了,最后的步骤我们看到,函数中调用了 prevScheduledCallback,那这里的 prevScheduledCallback 到底是什么来的?

其实这里的 prevScheduledCallback 就是接下来要解释的 flushWork 函数,并且执行该函数的时候,传入了 didTimeout 来进行判断是否超时了。

执行任务函数 flushWork

最后,React 通过上面的逻辑就实现了时间调度,对游览器每一帧上的空闲时间进行调度,接下来就是通过每次的调度后在空闲时间上执行 flushWork 来执行队列任务了。

shouldYieldToHost = function() {
    return frameDeadline <= getCurrentTime();
};
  
function flushWork(didTimeout) {
  // Exit right away if we're currently paused

  if (enableSchedulerDebugging && isSchedulerPaused) {
    return;
  }

  isExecutingCallback = true; // 开始真正调用 callback
  const previousDidTimeout = currentDidTimeout;
  currentDidTimeout = didTimeout;
  try {
    // didTimeout 是一个 boolean 值,用于判断调度任务是否超时了
    // 如果是,则需要执行该任务,否者该任务就要饿死了
    if (didTimeout) {
      // Flush all the expired callbacks without yielding.
      while (
        firstCallbackNode !== null &&
        !(enableSchedulerDebugging && isSchedulerPaused) // 链表循环判断节点或者是否暂停了
          );
      ) {
        // TODO Wrap in feature flag
        // Read the current time. Flush all the callbacks that expire at or
        // earlier than that time. Then read the current time again and repeat.
        // This optimizes for as few performance.now calls as possible.
        var currentTime = getCurrentTime();
        // 循环执行已经超时的所有任务
        if (firstCallbackNode.expirationTime <= currentTime) {
          do {
            flushFirstCallback();
          } while (
            firstCallbackNode !== null &&
            firstCallbackNode.expirationTime <= currentTime &&
            !(enableSchedulerDebugging && isSchedulerPaused)
          );
          continue;
        }
        break;
      }
    } else {
      // Keep flushing callbacks until we run out of time in the frame.
      if (firstCallbackNode !== null) {
        do {
          if (enableSchedulerDebugging && isSchedulerPaused) {
            break;
          }
          flushFirstCallback();
          // shouldYieldToHost 用于判断是否还有剩余时间执行任务
        } while (firstCallbackNode !== null && !shouldYieldToHost());
      }
    }
  } finally {
    isExecutingCallback = false;
    currentDidTimeout = previousDidTimeout;
    if (firstCallbackNode !== null) {
      // There's still work remaining. Request another callback.
      ensureHostCallbackIsScheduled();
    } else {
      isHostCallbackScheduled = false;
    }
    // Before exiting, flush all the immediate work that was scheduled.
    flushImmediateWork();
  }
}

进入函数通过判断 didTimeout 如果为 true,表示 调度任务已经超时,说明这个任务已经等太久了,再不让执行就要饿死了!因此,便获得了在不打断的情况下执行所有已超时的任务的权限。

若当前调度尚未超时,则在规定的时效内(shouldYieldToHost当前时间小于 frameDeadline 下一帧之前 ),尽可能多的执行任务。当该次调度执行完毕(不管是任务执行完或者因为中断暂停执行),在任务队列还有任务需要执行的时候重新执行 ensureHostCallbackIsScheduled 为下一次的任务调度做准备。

总结

通过该文章对原来的解析,总算是过了一遍 React Scheduler 的业务逻辑了,现在来看看一张流程图,更清晰的看看

image

参考文献