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
和之后都进行了日志记录promise2
-尽管这似乎是一种竞争状况。这真的很奇怪,因为Firefox 39和Safari 8.0.7始终如一地正确。
要了解这一点,您需要了解事件循环如何处理任务和微任务。第一次遇到这个问题可能会让您大吃一惊。深呼吸…
每个“线程”都有自己的事件循环,因此每个Web工作者都有自己的事件循环,因此可以独立执行,而同一源上的所有窗口都可以共享事件循环,因为它们可以同步通信。事件循环持续运行,执行所有排队的任务。事件循环具有多个任务源,这些任务源保证了该源中的执行顺序(如IndexedDB之类的规范定义了它们的执行顺序),但是浏览器可以在循环的每个循环中选择从哪个源中执行任务。这使浏览器可以优先执行对性能敏感的任务,例如用户输入。好吧好吧,和我在一起…
计划了任务,以便浏览器可以从内部访问JavaScript / DOM,并确保这些操作顺序发生。在任务之间,浏览器可以呈现更新。从鼠标单击到事件回调,与分析HTML一样需要安排任务,在上例中为setTimeout
。
setTimeout
等待给定的延迟,然后为其回调安排新任务。这就是为什么setTimeout
在之后script end
进行记录的原因,因为日志记录script end
是第一个任务的一部分,并setTimeout
记录在单独的任务中。是的,我们几乎已经完成了这一步,但我需要您在接下来的这段时间内保持坚强……
Microtasks通常安排事情,应该当前执行脚本后直发生,如反应批量的行动,或使一些异步而不采取一个全新的任务的处罚。只要没有其他JavaScript在执行中间,微任务队列就会在回调之后进行处理,并且在每个任务结束时进行处理。在微任务期间排队的所有其他微任务都将添加到队列的末尾并进行处理。微任务包括变异观察者回调,并如上例所示,承诺回调。
一旦承诺达成,或者如果已经达成,它将对微任务排队以进行其反动回调。这样可以确保即使promise已经解决,promise回调也是异步的。因此,.then(yey, nay)
对已解决的诺言进行调用会立即使微任务排队。这就是为什么promise1
并promise2
在之后记录日志的原因script end
,因为当前正在运行的脚本必须在处理微任务之前完成。promise1
并且promise2
在之前记录setTimeout
,因为微任务总是在下一个任务之前发生。
因此,请逐步:
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
,setTimeout
,promise1
,promise2
。他们在之后运行promise回调setTimeout
。他们可能将promise回调称为新任务的一部分,而不是微任务。
这是可以原谅的,因为承诺来自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上下文中,“可以存在”变为“必须存在”。
Firefox和Safari正确耗尽了点击侦听器之间的微任务队列,如突变回调所示,但承诺的排队似乎不同。鉴于工作和微任务之间的联系模糊,这是可以原谅的,但我仍然希望它们在侦听器回调之间执行。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,
});
所以,正确的顺序是:click
,click
,promise
,mutate
,promise
,timeout
,timeout
,因此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中事情总是失败的,因为在回调之后无法处理突变事件。
希望我们很快会在这里开始看到一些互操作性。
综上所述: