详解JavaScript的任务、微任务、队列以及代码执行顺序

为了保证的可读性,本文采用意译而非直译。html

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!前端

思考下面 JavaScript 代码:

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

控制台打印的顺序是怎样的?git

答案

正确的答案是:script start, script end, promise1, promise2, setTimeout,可是因为浏览器实现支持不一样致使结果也不一致。github

Microsoft Edge、Firefox 40、iOS Safari和桌面Safari 8.0.8 打印promise1promise2以前会先打印 setTimeout —— 这彷佛是浏览器厂商相互竞争致使的实现不一样。这真的很奇怪,由于 Firefox 39 和 Safari 8.0.7 结果老是正确的。web

为何会这样

要理解这一点,须要了解事件循环<event loop>如何处理任务和微任务。chrome

每一个“线程”都有本身的事件循环<event loop>,所以每一个 web worker 都有本身的事件循环,所以能够独立执行,而来自同域的全部窗口共享一个事件循环,因此它们能够同步地通讯。api

事件循环持续运行,直到清空 Tasks 队列的任务。一个事件循环有多个任务源,这些任务源保证了该源中的执行顺序(好比IndexedDB定义了它们本身的规范),可是浏览器能够在每次循环中选择哪一个源来执行任务。这容许浏览器优先选择性能敏感的任务,好比用户输入等。promise

Tasks 被放到任务源中,这样浏览器就能够从内部进入JavaScript/DOM领域,并确保这些操做按顺序进行。在Tasks 执行期间,浏览器可能更新渲染。从鼠标点击到事件回调须要调度一个任务,解析超文本标记语言也是如此。浏览器

setTimeout迟给定的时间,而后为它的回调调度一个新任务。这就是为何setTimeout在打印script end以后打印,由于打印script end是第一个任务的一部分,而setTimeout在一个单独的任务中。app

微任务<Microtasks>一般是针对当前执行脚本以后应该当即发生的事情进行调度的,好比对一批操做进行响应,或者在不影响整个新任务的状况下进行异步处理。

只要没有其余JavaScript处于执行中期,而且在每一个任务的末尾,微任务队列就在回调以后处理。在微任务期间排队的任何其余微任务都会被添加到队列的末尾并进行处理。微任务 包括 MutationObserver callbacks。例如上面的例子中的 promisecallback

一个settled状态的promise 或者已经变成settled状态(异步请求被settled)的promise,会马上将它的callback(then)放到微任务队列里面。

这确保了 promise 回调是异步的,即使promise已经变为settled状态。所以一个已settledpromise调用.then(yey,nay)时将当即把一个微任务加入微任务队列中。

这就是为何promise1promise2会在script end后打印,由于当前运行的脚本必须在处理微任务以前完成。promise1promise2setTimeout以前打印,由于微任务老是在下一个任务以前发生。

好,一步一步的运行:

图片描述

浏览器之间会有什么不一样?

一些浏览器的打印的顺序是 script start, script end, setTimeout, promise1, promise2。它们在setTimeout以后运行promise回调。极可能他们调用promise回调是做为新任务的一部分,而不是做为一个微任务。

这也是能够理解的,由于promise来自 ECMAScript 而不是 HTML。ECMAScript 有“做业”的概念,相似于微任务,可是除了模糊的邮件列表讨论以外,这种关系并不明确。然而,广泛的共识是,promise应该是微任务队列的一部分而且有充足的理由。

promise 看做任务会致使性能问题,由于回调没有必要由于任务相关的事(好比渲染)而延迟执行。它还会因为与其余任务源的交互而致使非肯定性,并可能中断与其余api的交互,稍后将详细介绍。

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

如何判断某些东西是否使用任务或微任务

动手试一试是一种办法,查看相对于promisesetTimeout如何打印,尽管这取决于实现是否正确。

一种方法是查看规范: 将一个任务加入队列: step 14 of setTimeout

将 microtask 加入队列:step 5 of queuing a mutation record

如上所述,ECMAScript 将微任务称为做业: 调用 EnqueueJob 将一个 微任务加入队列:step 8.a of PerformPromiseThen

等级一 boss打怪

下面是一段html代码:

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

给出下面的JS代码,若是点击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);

在偷看答案前先试一试

试一试

图片描述

和你猜测的有不一样吗?若是是,你获得的结果可能也是正确的。不幸的是,浏览器实现并不统一,下面是各个浏览器下测试结果:

clipboard.png

谁是正确的?

调度'click'事件是一项任务。 Mutation observer 和 promise 回调被列为微任务。 setTimeout 回调列为任务。 所以运行过程以下:

图片描述

因此 Chrome 是对的。对我来讲新发现是,微任务在回调以后运行(只要没有其它的 Javascript 在运行),我原觉得它只能在一个任务的末尾执行。

浏览器出了什么问题?

对于 mutation callbacks,Firefox 和 Safari 都正确地在内部区域和外部区域单击事件之间执行完毕,清空了微任务队列,可是 promises 列队的处理看起来和chrome不同。这多少情有可原,由于做业和微任务的关系不清楚,可是我仍然指望在事件回调之间处理 Firefox ticket. Safari ticket.

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

等级一 boss打怪升级

仍然使用上面的例子,假如咱们运行下面代码会怎么样:

inner.click();

跟以前同样,它会触发 click 事件,但此次是经过 JS 调用的。

试一试

图片描述

下面是各个浏览器的运行状况:

clipboard.png

我发誓我一直在从Chrome中获得不一样的结果,我已经更新了这张图表不少次了,我觉得我在错误地测试Canary。若是你在Chrome中获得不一样的结果,请在评论中告诉我是哪一个版本。

为何不一样?

应该是这样的:

图片描述

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

之前,这意味着微任务在侦听器回调之间运行,但.click()会致使事件同步调度,所以调用.click()的脚本仍然在回调之间的堆栈中。 上述规则确保微任务不会中断执行中期的JavaScript。 这意味着咱们不处理侦听器回调之间的微任务队列,它们在两个侦听器以后处理。

总结

任务按顺序执行,浏览器能够在它们之间进行渲染:

微任务按顺序执行,并执行:

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

代码部署后可能存在的BUG无法实时知道,过后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具 Fundebug

交流

干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,便可看到福利,你懂的。

clipboard.png

相关文章
相关标签/搜索