任务,微任务,队列和时间表

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 startscript endpromise1promise2setTimeout,但它在浏览器支持方面至关野生那里。javascript

Microsoft Edge,Firefox 40,iOS Safari和桌面Safari 8.0.8 setTimeout以前promise1和以后都进行了日志记录promise2-尽管这彷佛是一种竞争情况。这真的很奇怪,由于Firefox 39和Safari 8.0.7始终如一地正确。html

为何会这样

要了解这一点,您须要了解事件循环如何处理任务和微任务。第一次遇到这个问题可能会让您大吃一惊。深呼吸…java

每一个“线程”都有本身的事件循环,所以每一个Web工做者都有本身的事件循环,所以能够独立执行,而同一源上的全部窗口均可以共享事件循环,由于它们能够同步通讯。事件循环持续运行,执行全部排队的任务。事件循环具备多个任务源,这些任务源保证了该源中的执行顺序(如IndexedDB之类的规范定义了它们的执行顺序),可是浏览器能够在循环的每一个循环中选择从哪一个源中执行任务。这使浏览器能够优先执行对性能敏感的任务,例如用户输入。好吧好吧,和我在一块儿…git

计划了任务,以便浏览器能够从内部访问JavaScript / DOM,并确保这些操做顺序发生。在任务之间,浏览器能够呈现更新。从鼠标单击到事件回调,与分析HTML同样须要安排任务,在上例中为setTimeoutes6

setTimeout等待给定的延迟,而后为其回调安排新任务。这就是为何setTimeout在以后script end进行记录的缘由,由于日志记录script end是第一个任务的一部分,并setTimeout记录在单独的任务中。是的,咱们几乎已经完成了这一步,但我须要您在接下来的这段时间内保持坚强……github

Microtasks一般安排事情,应该当前执行脚本后直发生,如反应批量的行动,或使一些异步而不采起一个全新的任务的处罚。只要没有其余JavaScript在执行中间,微任务队列就会在回调以后进行处理,而且在每一个任务结束时进行处理。在微任务期间排队的全部其余微任务都将添加到队列的末尾并进行处理。微任务包括变异观察者回调,并如上例所示,承诺回调。promise

一旦承诺达成,或者若是已经达成,它将对微任务排队以进行其反动回调。这样能够确保即便promise已经解决,promise回调也是异步的。所以,.then(yey, nay)对已解决的诺言进行调用会当即使微任务排队。这就是为何promise1promise2在以后记录日志的缘由script end,由于当前正在运行的脚本必须在处理微任务以前完成。promise1而且promise2在以前记录setTimeout,由于微任务老是在下一个任务以前发生。浏览器

所以,请逐步:app

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 startscript endsetTimeoutpromise1promise2。他们在以后运行promise回调setTimeout。他们可能将promise回调称为新任务的一部分,而不是微任务。dom

这是能够原谅的,由于承诺来自ECMAScript而不是HTML。ECMAScript具备相似于微型任务的“任务”概念,可是除了模糊的邮件列表讨论以外,这种关系并无明确。可是,广泛的共识是,应将诺言做为微任务队列的一部分,这是有充分理由的。

将promise视为任务会致使性能问题,由于回调可能会因与任务相关的事情(例如渲染)而没必要要地延迟。因为与其余任务源的交互,它还会致使不肯定性,而且可能中断与其余API的交互,但稍后会介绍更多。

这是用于使用微任务进行承诺的Edge凭单。WebKit每晚都在作正确的事,所以我认为Safari最终会解决此问题,而且它彷佛已在Firefox 43中获得修复。

真正有趣的是,Safari和Firefox都在此发生了回归,此问题已获得修复。我想知道这是否只是一个巧合。

如何判断某物是使用任务仍是微任务

测试是一种方法。查看日志什么时候相对于promise&出现setTimeout,尽管您依靠的是正确的实现。

肯定的方法是查找规格。例如,ref="html.spec.whatwg.org/mu">步骤14setTimeout将任务排队,而将变异记录排队的步骤5将微任务排队。

如前所述,在ECMAScript领域中,他们称微任务为“工做”。在的f="ecma-international.org/">步骤8.a中PerformPromiseThen,EnqueueJob调用将微任务排队。

如今,让咱们看一个更复杂的例子。切向有关学徒, “不,他们还没准备好!”。别理他,你准备好了。咱们开工吧…

首先

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

javascript

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


图片


谁是对的?

调度“点击”事件是一项任务。变异观察者和promise回调做为微任务排队。该setTimeout回调排队的任务。因此这是怎么回事:

// 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能够正确处理。“对我来讲是新消息”的一点是,微任务是在回调以后处理的(只要没有其余JavaScript在中间执行),我认为它仅限于任务结束。此规则来自HTML规范,用于调用回调:

若是脚本设置对象堆栈如今为空,请执行微任务检查点
— HTML:在回调步骤3 以后进行清理

…而且微任务检查点涉及遍历微任务队列,除非咱们已经在处理微任务队列。相似地,ECMAScript对此做业说:

仅当没有正在运行的执行上下文而且执行上下文堆栈为空时才能够启动做业的执行。
— ECMAScript:做业和做业队列

…尽管在HTML上下文中,“能够存在”变为“必须存在”。

浏览器出了什么问题?

FirefoxSafari正确耗尽了点击侦听器之间的微任务队列,如突变回调所示,但承诺的排队彷佛不一样。鉴于工做和微任务之间的联系模糊,这是能够原谅的,但我仍然但愿它们在侦听器回调之间执行。Firefox票证。野生动物园门票。

使用Edge,咱们已经看到它的队列承诺不正确,可是它也没法耗尽点击侦听器之间的微任务队列,相反,它是在调用全部侦听器以后执行的,这mutate在两个click日志以后占单个日志。错误票。

使用上面的相同示例,若是执行如下命令会发生什么:

inner.click()

这将像之前同样开始事件调度,可是使用脚本而不是真正的交互。

图片

图片


Why is it different?

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



图片


因此,正确的顺序是:clickclickpromisemutatepromisetimeouttimeout,所以Chrome彷佛获得正确的。

在调用每一个侦听器回调以后……

若是脚本设置对象堆栈如今为空,请执行微任务检查点
— HTML:在回调步骤3 以后进行清理

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

有什么关系吗?

是的,它会在不起眼的地方(哎呀)咬你。我在尝试为使用Promise而非怪异IDBRequest对象的IndexedDB建立简单包装库时遇到了此问题。它 href="github.com/jakearchibal">几乎使IDB使用起来颇有趣。

当IDB触发成功事件时,相关的事务对象在分派后变为非活动状态(步骤4)。若是我建立了一个在事件触发时解决的Promise,则回调应在事务仍处于活动状态时在第4步以前运行,可是在Chrome之外的其余浏览器中不会发生,这会使库有点用。

实际上,您能够在Firefox中解决此问题,由于诸如es6-promise之类的承诺填充将突变观察者用于回调,而回调正确地使用了微任务。Safari彷佛因该修复程序而遭受竞争条件的折磨,但这可能只是IDB的无效实现。不幸的是,在IE / Edge中事情老是失败的,由于在回调以后没法处理突变事件。

但愿咱们很快会在这里开始看到一些互操做性。

你作到了!

综上所述:

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

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

    • 在每次回调以后,只要没有其余JavaScript在执行中间

    • 在每一个任务结束时


但愿您如今了解事件循环的方式,或者至少有借口能够躺下。

相关文章
相关标签/搜索