Tasks(任务), microtasks(微任务), queues(队列) and schedules(回调队列)

若是你更喜欢视频,Philip Roberts 在 JSConf 上就事件循环有一个很棒的演讲——没有讲 microtasks,不过很好的介绍了其它概念。好,继续!html

试一下

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,可是各浏览器不一致。git

Microsoft Edge, Firefox 40, iOS Safari 及桌面 Safari 8.0.8 在 promise1 和 promise2 以前打印 setTimeout ——尽管这彷佛是竞争条件致使的。奇怪的是,Firefox 39 和 Safari 8.0.7 是对的。es6

为何是这样的

要想弄明白为何,你须要知道事件循环如何处理 tasks 和 microtasks。第一次接触须要花些功夫才能弄明白。深呼吸……github

每一个线程都有本身的事件循环,因此每一个 web worker 有本身的事件循环(event loop),因此它能独立地运行。而全部同源的 window 共享一个事件循环,由于它们能同步的通信。事件循环持续运行,执行 tasks 列队。一个事件循环有多个 task 来源,而且保证在 task 来源内的执行顺序(IndexedDB 等规范定义了本身的 task 来源),在每次循环中浏览器要选择从哪一个来源中选取 task,这使得浏览器能优先执行敏感 task,例如用户输入。Ok ok, 留下来陪我坐会儿……web

Tasks 被列入队列,因而浏览器能从它的内部转移到 Javascript/DOM 领地,而且确使这些 tasks 按序执行。在 tasks 之间,浏览器能够更新渲染。来自鼠标点击的事件回调须要安排一个 task,解析 HTML 和 setTimeout 一样须要。api

setTimeout 延迟给定的时间,而后为它的回调安排一个新的 task。这就是为何 setTimeout 在 script end 以后打印,script end 在第一个 task 内,setTimeout 在另外一个 task 内。好了,咱们快讲完了,剩下一点我须要大家坚持下……promise

Mircotasks 一般用于安排一些事,它们应该在正在执行的代码以后当即发生,例如响应操做,或者让操做异步执行,以避免付出一个全新 task 的代价。mircotask 队列在回调以后处理,只要没有其它执行当中的(mid-execution)代码;或者在每一个 task 的末尾处理。在处理 microtasks 队列期间,新添加的 microtasks 添加到队列的末尾而且也被执行。 microtasks 包括 mutation observer 回调。上面的例子中的 promise 的回调也是。浏览器

promise 一旦解决(settled),或者已解决,它便为它的回调安排一个 microtask。这确使 promise 回调是异步的,即使 promise 已经解决。所以一个已解决的 promise 调用 .then(yey, nay) 将当即把一个 microtask 加入队列。这就是为何 promise1 和 promise2 在 script end 以后打印,由于正在运行的代码必须在处理 microtasks 以前完成。promise1 和 promise2 在 setTimeout 以前打印,由于 microtasks 老是在下一个 task 以前执行。app

好吧,一步一步运行dom

console.log('script start');

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

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

(注:原文表示运行结果为:'script start', 'script end', 'promise1', 'promise2', 'setTimeout')

为何有些浏览器表现不一致

一些浏览器的打印结果:script start, script end, setTimeout, promise1, promise2。在 setTimeout 以后运行 promise 的回调,就好像将 promise 的回调看成一个新的 task 而不是 microtask。

这多少情有可原,由于 promise 来自 ECMAScript 规范而不是 HTML 规范。ECAMScript 有一个概念 job,和 microtask 类似,可是二者的关系在邮件列表讨论中没有明确。不过,通常共识是 promise 应该是 microtask 队列的一部分,而且有充足的理由。

将 promise 看成 task 会致使性能问题,由于回调可能没必要要地被与 task 相关的事(好比渲染)延迟。与其它 task 来源交互时它也致使不肯定性,也会打断与其它 API 的交互,不事后面再细说。

我提交了一条 Edge 反馈,它错误地将 promises 看成 task。WebKit nightly 作对了,因此我认为 Safari 最终会修复,而 Firefox 43 彷佛已经修复。

有趣的是 Safari 和 Firefox 发生了退化,而以前的版本是对的。我在想这是否只是巧合。

怎么判断是task仍是mictask

测试是一种办法,查看相对于 promise 和 setTimeout 如何打印,尽管这取决于实现是否正确。

一种方法是查看规范。例如,setTimeout 的第十四步将一个 task 加入队列,mutation record 的第五步将 microtask 加入队列。

如上所述,ECMAScript 将 microtask 称为 job。PerformPromiseThen 的第八步 调用 EnqueueJob 将一个 microtask 加入队列。

如今,让咱们看一个更复杂的例子。(一个有心的学徒 :“可是他们尚未准备好”。别管他,你已经准备好了,让咱们开始……)

level 1

在写这篇文章以前我一直 会在这里出错。下面是 html 代码片断:

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

有以下的 Javascript 代码,假如我点击 div.inner 会打印什么?

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

在给出答案前先实际运行一下吧

测试

点击里面的矩形触发一个 click 事件:

你的猜想是否不一样?如果,你也多是对的。但不幸的是各浏览器不一致:

图片描述 图片描述 图片描述 图片描述
click click click click
promise mutate click mutate
mutate click mutate click
click mutate timeout mutate
promise timeout promise promise
mutate promise timeout promise
timeout promise promise timeout
timeout timeout timeout

哪一个是正确的

一个 microtask checkpoint 逐个检查 microtask 队列,除非咱们已经在处理一个 microtask 队列。相似地,ECMAScript 规范这么说 jobs:

Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty…

ECMAScript: Jobs and Job Queues
触发 click 事件是一个 task,Mutation observer 和 promise 回调做为 microtask 加入列队,setTimeout 回调做为 task 加入列队。所以运行过程以下:

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

因此 Chrome 是对的。对我来讲新发现是,microtasks 在回调以后运行(只要没有其它的 Javascript 在运行,我原觉得它只能在 task 的末尾运行。这个规则来自 HTML 规范,调用一个回调:

If the stack of script settings objects is now empty, perform a microtask checkpoint
— HTML: 回调以后的清理第三步

一个 microtask checkpoint 逐个检查 microtask 队列,除非咱们已经在处理一个 microtask 队列。相似地,ECMAScript 规范这么说 jobs:

Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty…

尽管在 HTML 中"can be"变成了"must be"

其它浏览器哪里错了了

对于 mutation 回调,Firefox 和 Safari 正确地在单击回调之间清空 microtask 队列,可是 promises 列队彷佛不同。这多少情有可原,由于 jobs 和 microtasks 的关系不清楚,可是我仍然指望在事件回调之间处理。Firefox bugSafari bug

对于 Edge,咱们已经看到它错误的将 promises 看成 task,它也没有在单击回调之间清空 microtask 队列,而是在全部单击回调执行完以后清空,因而总共只有一个 mutate 在两个 click 以后打印。 Edge bug

level 1 愤怒的老大哥

咱们用一样的例子运行:

inner.click();

跟以前同样,它会触发 click 事件,不过是经过代码而不是实际的交互动做。

试一下

图片描述 图片描述 图片描述 图片描述
click click click click
click click click click
promise mutate mutate mutate
mutate timeout timeout promise
promise promise promise promise
timeout promise timeout timeout
timeout timeout promise timeout

我发誓我在 Chrome 中始终获得不一样的结果,我更新了这个表许屡次才意识到我测试的是 Canary。假如你在 Chrome 中获得了不一样的结果,请在评论中告诉我是哪一个版本。

为何不一样

它应该像下面这样运行:

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

inner.click();

正确的顺序是:click, click, promise, mutate, promise, timeout, timeout,彷佛 Chrome 是对的。

在每一个事件回调调用以后:

If the stack of script settings objects is now empty, perform a microtask checkpoint.
— HTML: 回调以后的清理第三步

以前,这意味着 microtasks 在事件回调之间运行,可是 .click() 让事件同步触发,因此调用 .click() 的代码仍然在事件回调之间的栈内。上面的规则确保了 microtasks 不会中断执行当中的代码。这意味着 microtasks 队列在事件回调之间不处理,而是在它们以后处理。

这重要吗

重要,它会在偏角处咬你(疼)。我就遇到了这个问题,在我尝试用 promises 而不是用怪异的 IDBRequest 对象为 IndexedDB 建立一个简单的包装库 时。它让 IDB 用起来颇有趣

当 IDB 触发成功事件时,相关的 transaction 对象在事件以后转为非激活状态(第四步)。若是我建立的 promise 在这个事件发生时被履行(resolved),回调应当在第四步以前执行,这时这个对象仍然是激活状态。可是在 Chrome 以外的浏览器中不是这样,致使这个库有些无用。

实际上你能够在 Firefox 中解决这个问题,由于 promise polyfills 如 es6-promise 使用 mutation observers 执行回调,它正确地使用了 microtasks。而它在 Safari 下彷佛存在竞态条件,不过这多是由于他们 糟糕的 IDB 实现。不幸的是 IE/Edge 不一致,由于 mutation 事件不在回调以后处理。

但愿不久咱们能看到一些互通性。

你作到了!

总结:

  • tasks 按序执行,浏览器会在 tasks 之间执行渲染。
  • microtasks 按序执行,在下面状况时执行:

    • 在每一个回调以后,只要没有其它代码正在运行。
    • 在每一个 task 的末尾

但愿你如今明白了事件循环,或者至少获得一个借口出去走一走躺一躺。

相关文章
相关标签/搜索