任务队列

更复杂的就是,事件循环可以拥有多个任务队列。唯一的两个限制即是来自同一个任务源的事件必须属于同一个任务队列,并且每个队列中的所有的任务必须按插入的顺序执行。除了这些,用户代理可以随意做任何操作。例如,它可以决定下一个执行的任务队列。

  1. While (eventLoop.waitForTask()) {
  2. const taskQueue = eventLoop.selectTaskQueue()
  3. if (taskQueue.hasNextTask()) {
  4. taskQueue.processNextTask()
  5. }
  6. }

依据这个模型,我们就失去了对时间的精确控制。在执行我们用 setTimeout() 规划的任务之前,浏览器可能决定先完全处理其它的几个任务队列。

任务队列 - 图1

微任务队列

幸运的是,事件循环还有一个单一队列叫做微任务队列。在每个 tick 之中,每当当前任务执行完毕之后,微任务队列都被完全清空。

  1. while (eventLoop.waitForTask()) {
  2. const taskQueue = eventLoop.selectTaskQueue()
  3. if (taskQueue.hasNextTask()) {
  4. taskQueue.processNextTask()
  5. }
  6. const microtaskQueue = eventLoop.microTaskQueue
  7. while (microtaskQueue.hasNextMicrotask()) {
  8. microtaskQueue.processNextMicrotask()
  9. }
  10. }

设置一个微任务的最简单的方式是 Promise.resolve().then(microtaskFn)。微任务会按插入的顺序执行,因为只有一个微任务队列,所以用户代理这次不能干扰我们。

另外,微服务可以设置新的微服务插入到同一个微服务队列中,并且在同一个 tick 中执行。

任务队列 - 图2

渲染

最后一个需要注意的是渲染的时间表。不像事件处理或者解析,渲染不是由独立的后台任务来完成的。这是一种算法,可以运行在每一个循环 tick 结束。

用户代理又有很多自由的选择:它可能在每个任务之后渲染,但是它可能会决定执行成百上千的任务而不去渲染。

幸运的是有 requestAnimationFrame 函数,它会在下次渲染之前马上执行传入的函数。我们最终的事件循环模型如下所示:

  1. while (eventLoop.waitForTask()) {
  2. const taskQueue = eventLoop.selectTaskQueue()
  3. if (taskQueue.hasNextTask()) {
  4. taskQueue.processNextTask()
  5. }
  6. const microtaskQueue = eventLoop.microTaskQueue
  7. while (microtaskQueue.hasNextMicrotask()) {
  8. microtaskQueue.processNextMicrotask()
  9. }
  10. if (shouldRender()) {
  11. applyScrollResizeAndCSS()
  12. runAnimationFrames()
  13. render()
  14. }
  15. }

任务队列 - 图3

现在让我们利用所有这些知识来构建一个定时系统!