scheduler 源码

1,803 阅读12分钟

exports

exports.unstable_now
exports.unstable_forceFrameRate

exports.unstable_ImmediatePriority = ImmediatePriority;
exports.unstable_UserBlockingPriority = UserBlockingPriority;
exports.unstable_NormalPriority = NormalPriority;
exports.unstable_IdlePriority = IdlePriority;
exports.unstable_LowPriority = LowPriority;

exports.unstable_runWithPriority = unstable_runWithPriority;
exports.unstable_next = unstable_next;
exports.unstable_scheduleCallback = unstable_scheduleCallback;
exports.unstable_cancelCallback = unstable_cancelCallback;
exports.unstable_wrapCallback = unstable_wrapCallback;
exports.unstable_getCurrentPriorityLevel = unstable_getCurrentPriorityLevel;
exports.unstable_shouldYield = unstable_shouldYield;
exports.unstable_requestPaint = unstable_requestPaint;
exports.unstable_continueExecution = unstable_continueExecution;
exports.unstable_pauseExecution = unstable_pauseExecution;
exports.unstable_getFirstCallbackNode = unstable_getFirstCallbackNode;
exports.unstable_Profiling = unstable_Profiling;

源码说明

  • 重点是 unstable_scheduleCallback 函数,实现了任务调度。
  • 其他函数只是对外提供的一些调度操作。

优先级

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

// 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;

function timeoutForPriorityLevel(priorityLevel) {
  switch (priorityLevel) {
    case ImmediatePriority:
      return IMMEDIATE_PRIORITY_TIMEOUT;

    case UserBlockingPriority:
      return USER_BLOCKING_PRIORITY;

    case IdlePriority:
      return IDLE_PRIORITY;

    case LowPriority:
      return LOW_PRIORITY_TIMEOUT;

    case NormalPriority:
    default:
      return NORMAL_PRIORITY_TIMEOUT;
  }
}

peek、push、pop、siftUp、siftDown

[Scheduler] Store Tasks on a Min Binary Heap #16245

Switches Scheduler's priority queue implementation (for both tasks and timers) to an array-based min binary heap.

This replaces the naive linked-list implementation that was left over from the queue we once used to schedule React roots. A list was arguably fine when it was only used for roots, since the total number of roots is usually small, and is only 1 in the common case of a single-page app.

Since Scheduler is now used for many types of JavaScript tasks (e.g. including timers), the total number of tasks can be much larger.

Heaps are the standard way to implement priority queues. Insertion is O(1) in the average case (append to the end) and O(log n) in the worst. Deletion is O(log n). Peek is O(1).

翻译

  • 将调度程序的优先级队列实现(对于任务和计时器)切换到基于数组的最小二进制堆。
  • 这将取代最初的链接列表实现,这是我们曾经用来调度React根目录的队列中剩下的东西。当列表仅用于根目录时,可以说很好,因为根目录的总数通常很小,在单页面应用程序的常见情况下只有1。
  • 由于Scheduler现在用于许多类型的JavaScript任务(例如,包括计时器),因此任务总数可能会大得多。
  • 堆是实现优先级队列的标准方法。一般情况下(插入到末尾)插入为O(1),最坏情况下插入为O(log n),删除为O(log n),查看是O(1)。
// Tasks are stored on a min heap
// 任务存储在最小堆中 
var taskQueue = [];
var timerQueue = [];

// 入堆新节点
function push(heap, node) {
  var index = heap.length;
  heap.push(node);
  siftUp(heap, node, index);
}

function peek(heap) {
  var first = heap[0];
  return first === undefined ? null : first;
}

// 删除根节点
function pop(heap) {
  var first = heap[0];

  if (first !== undefined) {
    var last = heap.pop();

    if (last !== first) {
      heap[0] = last;
      siftDown(heap, last, 0);
    }

    return first;
  } else {
    return null;
  }
}

// 权重小的上浮
function siftUp(heap, node, i) {
  var index = i;

  while (true) {
    var parentIndex = Math.floor((index - 1) / 2);
    var parent = heap[parentIndex];

    if (parent !== undefined && compare(parent, node) > 0) {
      // The parent is larger. Swap positions.
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      // The parent is smaller. Exit.
      return;
    }
  }
}

// 权重大的下沉
function siftDown(heap, node, i) {
  var index = i;
  var length = heap.length;

  while (index < length) {
    var leftIndex = (index + 1) * 2 - 1;
    var left = heap[leftIndex];
    var rightIndex = leftIndex + 1;
    var right = heap[rightIndex]; 
    // If the left or right node is smaller, swap with the smaller of those.
    if (left !== undefined && compare(left, node) < 0) {
      if (right !== undefined && compare(right, left) < 0) {
        heap[index] = right;
        heap[rightIndex] = node;
        index = rightIndex;
      } else {
        heap[index] = left;
        heap[leftIndex] = node;
        index = leftIndex;
      }
    } else if (right !== undefined && compare(right, node) < 0) {
      heap[index] = right;
      heap[rightIndex] = node;
      index = rightIndex;
    } else {
      // Neither child is smaller. Exit.
      return;
    }
  }
}

function compare(a, b) {
  // Compare sort index first, then task id.
  var diff = a.sortIndex - b.sortIndex;
  return diff !== 0 ? diff : a.id - b.id;
}

unstable_scheduleCallback

// Incrementing id counter. Used to maintain insertion order.
//递增ID计数器。用于维护插入顺序。 
var taskIdCounter = 1;
var enableProfiling = true;
// This is set while performing work, to prevent re-entrancy.
//这是在执行工作时设置的,以防止重新进入。 
var isPerformingWork = false;
var isHostCallbackScheduled = false;
function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = exports.unstable_now();
  var startTime;
  var timeout;

  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;

    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }

    timeout = typeof options.timeout === 'number' ? options.timeout : timeoutForPriorityLevel(priorityLevel);
  } else {
    timeout = timeoutForPriorityLevel(priorityLevel);
    startTime = currentTime;
  }

  var expirationTime = startTime + timeout;
  var newTask = {
    id: taskIdCounter++,
    callback: callback,
    priorityLevel: priorityLevel,
    startTime: startTime,
    expirationTime: expirationTime,
    sortIndex: -1
  };

  if (enableProfiling) {
    newTask.isQueued = false;
  }

  if (startTime > currentTime) {
    // This is a delayed task.
    newTask.sortIndex = startTime;
    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);

    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    } 
    // Schedule a host callback, if needed. If we're already performing work,
    // wait until the next time we yield.
    // 如果需要安排主回调。如果我们已经在做工作,等待下一次我们 yield。 
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

源码说明

scheduler 内部维护了两个数组:timerQueue 和 taskQueue

  • timerQueue用来存放延迟任务(delay task),即startTime > currentTime的任务;
  • taskQueue用来存放即时任务。
  • timerQueue 和 taskQueue 都是小根堆。

enableProfiling 主要用于性能分析,先忽略;

isHostCallbackScheduled 的作用是标记是否安排了主任务。

  1. 调用 unstable_now() 拿到 currentTime;
  2. 根据传入的优先级和 options 配置,设置 startTime、timeout;
  3. 计算过期时间 expirationTime = startTime + timeout;
  4. 新建一个任务 newTask;
  5. 延迟任务:
    1. 将任务的 sortIndex 设置为 startTime;
    2. 将任务添加到 timerQueue 中,并保持最小堆结构;
    3. 如果当前 taskQueue 为空,即无即时任务,并且当前新任务的优先级是最高
      1. 取消之前的延迟任务;
      2. 调用 requestHostTimeout , 延迟时间为新任务的 startTime - currentTime ,经过该时间之后执行 handleTimeout;
  6. 非延迟任务:
    1. 将任务的 sortIndex 设置为 expirationTime;
    2. 将 newTask 推入 taskQueue,并使用 siftUp 将权重小的上浮;
    3. 调用 requestHostCallback(flushWork);

辅助函数

var requestHostCallback;
var requestHostTimeout;
var cancelHostTimeout;
var shouldYieldToHost;
var requestPaint;

exports.unstable_now

源码说明

以上几个函数,在不同情形下,应用的基础不同,下面分情况详解。

在非 DOM 环境或不支持 MessageChannel 的环境

// If Scheduler runs in a non-DOM environment, it falls back to a naive
// implementation using setTimeout.
// Check if MessageChannel is supported, too.
//如果Scheduler在非DOM环境或不支持MessageChannel的环境中运行,则会退回到原始,使用setTimeout实现。
if ( typeof window === 'undefined' || typeof MessageChannel !== 'function') {
  // If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
  // fallback to a naive implementation.
  var _callback = null;
  var _timeoutID = null;

  var _flushCallback = function () {
    if (_callback !== null) {
      try {
        var currentTime = exports.unstable_now();
        var hasRemainingTime = true;

        _callback(hasRemainingTime, currentTime);

        _callback = null;
      } catch (e) {
        setTimeout(_flushCallback, 0);
        throw e;
      }
    }
  };

  var initialTime = Date.now();

  exports.unstable_now = function () {
    return Date.now() - initialTime;
  };

  requestHostCallback = function (cb) {
    if (_callback !== null) {
      // Protect against re-entrancy.
      setTimeout(requestHostCallback, 0, cb);
    } else {
      _callback = cb;
      setTimeout(_flushCallback, 0);
    }
  };

  requestHostTimeout = function (cb, ms) {
    _timeoutID = setTimeout(cb, ms);
  };

  cancelHostTimeout = function () {
    clearTimeout(_timeoutID);
  };

  shouldYieldToHost = function () {
    return false;
  };

  requestPaint = exports.unstable_forceFrameRate = function () {};
}

源码说明

  1. unstable_now 返回 Date.now() - initialTime;initialTime 是 React 应用初始化时就会生成的一个变量,值也是 Date.now(),并且这个值不会在后期再被改变。那么 Date.now() - initialTime 这两个值相减以后,得到的结果也就是现在离 React 应用初始化时经过了多少时间;无论如何 now() 获取到的值都是越来越大的。
  2. requestHostCallback、requestHostTimeout、cancelHostTimeout:都使用 setTimeout 实现调度;
  3. shouldYieldToHost:始终返回 false,不需要暂停;

在 DOM 且支持 MessageChannel 的环境

unstable_now & shouldYieldToHost & requestPaint

var enableIsInputPending = false;


if ( typeof window === 'undefined' || typeof MessageChannel !== 'function') {
  //...
} else {
  // Capture local references to native APIs, in case a polyfill overrides them.
  //获取对本地API的本地引用,以防polyfill覆盖它们。
  var performance = window.performance;
  var _Date = window.Date;
  var _setTimeout = window.setTimeout;
  var _clearTimeout = window.clearTimeout;
  //...
  if (typeof performance === 'object' && typeof performance.now === 'function') {
    exports.unstable_now = function () {
      return performance.now();
    };
  } else {
    var _initialTime = _Date.now();

    exports.unstable_now = function () {
      return _Date.now() - _initialTime;
    };
  }

  var deadline = 0;
  var maxYieldInterval = 300;
  var needsPaint = false;

  if (enableIsInputPending && navigator !== undefined && navigator.scheduling !== undefined && navigator.scheduling.isInputPending !== undefined) {
    var scheduling = navigator.scheduling;

    shouldYieldToHost = function () {
      var currentTime = exports.unstable_now();

      if (currentTime >= deadline) {
        // There's no time left. We may want to yield control of the main
        // thread, so the browser can perform high priority tasks. The main ones
        // are painting and user input. If there's a pending paint or a pending
        // input, then we should yield. But if there's neither, then we can
        // yield less often while remaining responsive. We'll eventually yield
        // regardless, since there could be a pending paint that wasn't
        // accompanied by a call to `requestPaint`, or other main thread tasks
        // like network events.
        //没有时间了。我们可能想让出主线程,
        //因此浏览器可以执行高优先级任务。主要是渲染和用户输入。
        //如果有待处理的渲染或待处理输入,
        //那么我们应该暂停。但是如果两者都不存在,那么我们可以
        //在保持响应速度的同时降低暂停的频率。
        //我们最终无论如何都会暂停,因为可能存在尚未提交的待定渲染
        //伴随调用`requestPaint`或其他主线程任务
        //就像网络事件一样。 
        if (needsPaint || scheduling.isInputPending()) {
          // There is either a pending paint or a pending input.
          return true;
        } 
        // There's no pending input. Only yield if we've reached the max
        // yield interval.
        //没有待处理的输入。只有在达到最大值时才屈服
        //收益区间。
        return currentTime >= maxYieldInterval;
      } else {
        // There's still time left in the frame.
        return false;
      }
    };

    requestPaint = function () {
      needsPaint = true;
    };
  } else {
    // `isInputPending` is not available. Since we have no way of knowing if
    // there's pending input, always yield at the end of the frame.
    //`isInputPending`不可用。因为我们无法知道是否有待处理的输入,所以始终在帧末尾暂停。 
    shouldYieldToHost = function () {
      return exports.unstable_now() >= deadline;
    }; 
    // Since we yield every frame regardless, `requestPaint` has no effect.
    //由于无论如何我们每一帧都会暂停,因此`requestPaint`无效。 
    requestPaint = function () {};
  }
  
  //...
}

源码说明

在 DOM 且支持 MessageChannel 的环境下逻辑比较复杂,我们先看 unstable_now、shouldYieldToHost、requestPaint 这三个函数:

  • unstable_now:如果支持 performance,返回 performance.now(),否则返回 Date.now() - initialTime

    performance.now() 与 Date.now 不同:

    • window.performance.now() 返回的时间戳没有被限制在一毫秒的精确度内,而它使用了一个浮点数来达到微秒级别的精确度。
    • window.performance.now() 是以一个恒定的速率慢慢增加的,它不会受到系统时间的影响(可能被其他软件调整)。

enableIsInputPending 为 false

  • shouldYieldToHost:超时就暂停。
  • requestPaint:空函数。

requestHostCallback & requestHostTimeout & cancelHostTimeout

if ( typeof window === 'undefined' || typeof MessageChannel !== 'function') {
  //...
} else {
  //...
  var isMessageLoopRunning = false;
  var scheduledHostCallback = null;
  var taskTimeoutID = -1; 
  // Scheduler periodically yields in case there is other work on the main
  // thread, like user events. By default, it yields multiple times per frame.
  // It does not attempt to align with frame boundaries, since most tasks don't
  // need to be frame aligned; for those that do, use requestAnimationFrame.
  //如果主线程上还有其他工作,调度程序会定期暂停,例如用户事件。
  //默认情况下,它每帧暂停多次。
  //它不会尝试与帧边界对齐,因为大多数任务不需要;
  //对于需要的任务,请使用requestAnimationFrame。
  var yieldInterval = 5;
  var deadline = 0;
  var maxYieldInterval = 300;
  var needsPaint = false;
  //...
  exports.unstable_forceFrameRate = function (fps) {
    if (fps < 0 || fps > 125) {
      console.error('forceFrameRate takes a positive int between 0 and 125, ' + 'forcing framerates higher than 125 fps is not unsupported');
      return;
    }

    if (fps > 0) {
      yieldInterval = Math.floor(1000 / fps);
    } else {
      // reset the framerate
      yieldInterval = 5;
    }
  };

  var performWorkUntilDeadline = function () {
    if (scheduledHostCallback !== null) {
      var currentTime = exports.unstable_now(); 
      // Yield after `yieldInterval` ms, regardless of where we are in the vsync
      // cycle. This means there's always time remaining at the beginning of
      // the message event.
      //在`yieldInterval` ms之后暂停,无论我们在vsync循环中处于什么位置。
      //这意味着在message 事件前总是有剩余的时间。
      deadline = currentTime + yieldInterval;
      var hasTimeRemaining = true;

      try {
        var hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);

        if (!hasMoreWork) {
          isMessageLoopRunning = false;
          scheduledHostCallback = null;
        } else {
          // If there's more work, schedule the next message event at the end
          // of the preceding one.
          //如果还有更多工作要做,请在最后安排下一个消息事件
          //前一个。 
          port.postMessage(null);
        }
      } catch (error) {
        // If a scheduler task throws, exit the current browser task so the
        // error can be observed.
        //如果抛出调度程序任务,请退出当前浏览器任务,以便
        //可以观察到错误。 
        port.postMessage(null);
        throw error;
      }
    } else {
      isMessageLoopRunning = false;
    } 
    // Yielding to the browser will give it a chance to paint, so we can
    // reset this.
    //屈服于浏览器将使其有渲染的机会,因此我们可以重置它。 
    needsPaint = false;
  };

  var channel = new MessageChannel();
  var port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;

  requestHostCallback = function (callback) {
    scheduledHostCallback = callback;

    if (!isMessageLoopRunning) {
      isMessageLoopRunning = true;
      port.postMessage(null);
    }
  };

  requestHostTimeout = function (callback, ms) {
    taskTimeoutID = _setTimeout(function () {
      callback(exports.unstable_now());
    }, ms);
  };

  cancelHostTimeout = function () {
    _clearTimeout(taskTimeoutID);

    taskTimeoutID = -1;
  };
}

源码说明

在 DOM 且支持 MessageChannel 的环境下,使用 MessageChannel 做任务调度:

  • performWorkUntilDeadline:
    • 如果存在 scheduledHostCallback,
      • 设置 deadline = currentTime + yieldInterval,即当前循环将在 5ms 之后暂停;
      • 然后调用 scheduledHostCallback(即 requestHostCallback 的回调:flushWork),返回是否还有工作要做。
        • 如果没有,表明 message 循环不需要再继续,退出循环;
        • 如果有新的任务,再触发一次 postMessage,再次执行 performWorkUntilDeadline。
    • 当 scheduledHostCallback 为空,退出循环。
  • requestHostCallback:请求执行主协程回调。
    • 将 scheduledHostCallback 设为 callback;
    • 如果当前 message 循环中没有任务执行,向管道中传入消息 null,触发 performWorkUntilDeadline 函数;
  • requestHostTimeout:使用 setTimeout 注册延迟任务。
  • cancelHostTimeout:清除延迟任务。

flushWork

function flushWork(hasTimeRemaining, initialTime) {
  if (enableProfiling) {
    markSchedulerUnsuspended(initialTime);
  } // We'll need a host callback the next time work is scheduled.


  isHostCallbackScheduled = false;

  if (isHostTimeoutScheduled) {
    // We scheduled a timeout but it's no longer needed. Cancel it.
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  isPerformingWork = true;
  var previousPriorityLevel = currentPriorityLevel;

  try {
    if (enableProfiling) {
      try {
        return workLoop(hasTimeRemaining, initialTime);
      } catch (error) {
        if (currentTask !== null) {
          var currentTime = exports.unstable_now();
          markTaskErrored(currentTask, currentTime);
          currentTask.isQueued = false;
        }

        throw error;
      }
    } else {
      // No catch in prod codepath.
      return workLoop(hasTimeRemaining, initialTime);
    }
  } finally {
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;

    if (enableProfiling) {
      var _currentTime = exports.unstable_now();

      markSchedulerSuspended(_currentTime);
    }
  }
}

源码说明

核心就是调用 workLoop,并返回是否还有工作要做。并更新 isHostCallbackScheduled 、 isHostTimeoutScheduled 和 isPerformingWork 的状态。

workLoop

function workLoop(hasTimeRemaining, initialTime) {
  var currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);

  while (currentTask !== null && !(enableSchedulerDebugging && isSchedulerPaused)) {
    if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost())) {
      // This currentTask hasn't expired, and we've reached the deadline.
      break;
    }

    var callback = currentTask.callback;

    if (callback !== null) {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      var didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      markTaskRun(currentTask, currentTime);
      var continuationCallback = callback(didUserCallbackTimeout);
      currentTime = exports.unstable_now();

      if (typeof continuationCallback === 'function') {
        currentTask.callback = continuationCallback;
        markTaskYield(currentTask, currentTime);
      } else {
        if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
        }

        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }

      advanceTimers(currentTime);
    } else {
      pop(taskQueue);
    }

    currentTask = peek(taskQueue);
  } 
  
  // Return whether there's additional work
  if (currentTask !== null) {
    return true;
  } else {
    var firstTimer = peek(timerQueue);

    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }

    return false;
  }
}

源码说明

  • 如果 taskQueue 中没有任务了,取 timerQueue 中的延时任务,
  • 如果 taskQueue、timerQueue 都为空,退出循环;
  • 如果 taskQueue 中有任务
    • 该任务没过期,但是 deadline 到了:要暂停执行,等下一次再进入 workLoop 时执行;
    • 如果任务已过期或还有空余时间,判断当前任务的 callback 是否为空;
      • 如果为空,直接将任务移出 taskQueue;
      • 如果 callback 不为空,调用 callback;
        1. 如果执行结果仍然是函数,将 continuationCallback 重新赋值给 currentTask.callback;
        2. 如果不是函数(且当前任务仍然是 taskQueue 中优先级最高的任务)将当前任务移出 taskQueue;
        3. 然后,根据新的 currentTime,调用 advanceTimers,判断是否需要将 timerQueue 中的任务转移到 taskQueue 中;
        4. 重新从 taskQueue 中取出任务,进入workLoop 中的 while 循环。

advanceTimers

function advanceTimers(currentTime) {
  // Check for tasks that are no longer delayed and add them to the queue.
  var timer = peek(timerQueue);

  while (timer !== null) {
    if (timer.callback === null) {
      // Timer was cancelled.
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // Timer fired. Transfer to the task queue.
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);

      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
      }
    } else {
      // Remaining timers are pending.
      return;
    }

    timer = peek(timerQueue);
  }
}

源码说明

主要是根据新的 currentTime,判断是否需要将 timerQueue 中的任务转移到 taskQueue 中。

handleTimeout

function handleTimeout(currentTime) {
  isHostTimeoutScheduled = false;
  advanceTimers(currentTime);

  if (!isHostCallbackScheduled) {
    if (peek(taskQueue) !== null) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    } else {
      var firstTimer = peek(timerQueue);

      if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}

源码说明

调用 advanceTimers,更新 taskQueue、timerQueue,重新取出任务作为主任务执行。

总结

  • timerQueue 用来存放延迟任务;
  • taskQueue 用来存放即时任务;
  • timerQueue 和 taskQueue 都是小根堆,所以每次取出的都是最紧急的任务。
  1. 有两层循环 MessageChannel 循环 和 workLoop 循环;
  2. 总是先执行 taskQueue 中的任务;
  3. 优先级:即时任务>延迟任务,所以有即时任务时,取消延迟任务的安排;
  4. taskQueue 中的一个任务执行完成后,不会立即移出,如果其 callback 返回的结果仍是函数,将其作为 callback 继续执行此任务;
  5. 当 timerQueue 中的任务过期后,会放入 taskQueue;
  6. taskQueue 是使用 MessageChannel 循环调用执行,但每 5ms 会暂停一次;暂停时如果 taskQueue 中有任务,下次 MessageChannel 循环的时候继续执行;如果 taskQueue 中已没有任务,安排 timerQueue 中的异步任务;
  7. 如果 taskQueue 为空,会使用 setTimeout 安排 timerQueue 中的异步任务。

在不支持 MessageChannel 的环境,taskQueue 中的任务也用 setTimeout 安排。

参考链接