js事件循环系统走马观花——Tasks, microtasks, queues and schedules

Event Loop 是js异步特性的基石,前端同学一定都耳熟能详。但是当我们把这些微操放在一起的时候,会是什么效果?你真的理解js这门语言异步的真谛么?先来个开胃菜:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

结果会输出什么?

正确的答案是: script start, script end, promise1, promise2, setTimeout

但如果你用Microsoft Edge, Firefox 40, iOS Safari 或者 Safari 8.0.8之前的版本,你可能会得到setTimeout 在 promise1前面的输出结果。

为什么?这里就得引入 tasks 和 microtasks 的概念了,正餐开始。。。

Tasks & Microtasks

每个thread或者说每个web worker都有一个自己的event loop。通过event loop来持续地执行队列中的任务。p.s: 同域名下的windows公用同一个event loop。

event loop 队列中的任务有多个来源,对于每次loop执行的优先级,浏览器执掌着最终的决定权。毕竟浏览器要决定哪些任务是比较敏感的,需要更高的优先级,比如说用户输入操作。

Task

浏览器会从计划队列中取出Task,并依次执行,在任务间隙,可能会渲染更新。
解析HTML、用户点击鼠标产生的callback,以及setTimeout等都会产生新的计划任务。
setTimeout在delay时间后悔计划一个新的task作为回调。

Microtasks

Microtasks指的是在当前执行脚本结束后应该立刻马上执行的计划。 所以只要没有中断脚本运行,Microtask的队列应该在callback之后立刻执行。这时候如果有更多的microtask加进来,应该也会一并执行,而不用等到下一个新的task阶段。
mutation observer的回调和上面例子中的promise的回调都属于Microtask。

综上,一旦promise resolve之后,会立刻将一个callback加入队列。这也是为什么promise一定是异步的,即使他里面包的内容实际上是同步的脚本。当我们调用.then((resolve, reject) => {})的时候,会在队列中立即产生新的Microtask。在上面例子中,当当前脚本执行完毕之后,也就是script end输出之后,会立刻执行promise产生的Microtask,也就是输出promise1promise2。只有当当前Task以及其中的Micritask结束之后,才会运行下一个Task中的setTimeout回调。

结论: * 当前的Task执行完毕之前注册的Microtask都会在下一个新的Task执行之前执行。

为什么在不同浏览器上结果会有差异呢?

有的浏览器中,运行上面脚本,setTimeout会在promise1之前输出。看上去好像是将promise callback当做Task而非Microtask。

我们知道promise来自ECMAScript,而非HTML。在ECMAScript中有jobs的概念,跟Microtask类似,但是这种对应关系并不明确。目前而言,大家统一的共识是promise应该作为microtask的一部分,而非task。

如果将promise的callback作为task,可能会导致性能问题。因为callback会很有可能被浏览器的task计划延后,例如渲染工作可以插入到promise的调用逻辑中间。也有可能造成未定义的结果。

相关标准

The certain way, is to look up the spec. For instance, step 14 of setTimeout queues a task, whereas step 5 of queuing a mutation record queues a microtask.
本着打破砂锅问到底的杠精心态,你可能会问了,js里面这么多异步,这么多的task和microtask,我咋知道哪个是哪个?

答案只有一个,查规范

在spec里面定义得可清楚了,这里是setTimeout的(文档)[https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-settimeout]
这里面节选第14条:

14.Return handle, and then continue running this algorithm (in parallel)[https://html.spec.whatwg.org/multipage/infrastructure.html#in-parallel].

做了什么呢?注意看上面quote的 in parallel

To run steps in parallel means those steps are to be run, one after another, at the same time as other logic in the standard (e.g., at the same time as the event loop). This standard does not define the precise mechanism by which this is achieved, be it time-sharing cooperative multitasking, fibers, threads, processes, using different hyperthreads, cores, CPUs, machines, etc. By contrast, an operation that is to run immediately must interrupt the currently running task, run itself, and then resume the previously running task.

这里实际上就是enqueue了一个task。

另外,我们再查一下(ECMAScript的文档)[http://www.ecma-international.org/ecma-262/6.0/#sec-performpromisethen]
为了大家的阅读体验,我截图如下:

ECMAScript

这里的EnqueueJob实际上就是enqueue 一个microtask

临终小试题

最后来一个稍微复杂一点的例子来测试本章所学。

首先是一个简单的html:

<div class="outer">
  <div class="inner"></div>
</div>

然后是另一段简单的js:

// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
  console.log('mutate');
}).observe(outer, {
  attributes: true
});

// Here's a click listener…
function onClick() {
  console.log('click');

  setTimeout(function() {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise');
  });

  outer.setAttribute('data-random', Math.random());
}

// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

好了,问题来了:

  1. 如果鼠标点击内部小方块,这段代码会输出什么?
  2. 如果直接js调用inner.click()呢?会不会有不同?为什么?

最后的最后,给点小提示吧:

1.If the stack of script settings objects is now empty, perform a microtask checkpoint
2. Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty…

js事件循环系统走马观花——Tasks, microtasks, queues and schedules
Share this