这种情况是可解释的,由于Promise来源于ECMAScript而不是HTML。ECMAScript拥有一个类似于microtasks(微任务)的jobs的概念。但是它与别的类型的任务的概念也不是太明确(参见vague mailing list discussions)。但是,在一般情况普遍认为Promise是微任务的一种。
// 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);
// 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
});
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
// 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
});
原文:Tasks, microtasks, queues and schedules。
当我跟我的同事Matt Gaunt交流的时候,我就在酝酿写一篇关于在浏览器环境下事件轮训的微任务(
microtasks
)队列执行顺序的文章。他说,我相信你会写的,但是,我肯定不会去阅读你的文章。那好,现在我无论如何都要把它写出来,以便于我们都能够静下来心平气和地讨论这个问题。事实上,如果你看过足够多的技术相关的视频,你一定看过这样一段great talk at JSConf on the event loop视频,虽然这段视频没有包含微任务(
microtasks
)的相关讲解,但它确实把异步相关的问题解释得较为清楚明白。看以下代码:
他们会以怎样的顺序输出呢?
事实上,正确的输出顺序是这样的:
但记住,这必须是对于支持
Promise()
语法的浏览器而言的。在
Microsoft Edge
,Firefox 40
,iOS Safari
以及 PC端的Safari 8.0.8
,setTimeout
会在promise1
和promise2
之前输出。这是十分奇怪的现象。但在Firefox 39
andSafari 8.0.7
似乎又能以正确的顺序输出。为什么会出现这样的情况?
为了理解为何会产生上述的输出结果,你需要知道
JavaScript引擎
是如何进行任务和微任务的事件轮询的。以下许多原理也许是你第一次接触到,保持冷静地看下去……每个线程都有他自己的事件轮询机制,每个页面都是在其所在线程独立运行的。然而,在一个页面中的所有
task
却共享同一个同步执行的线程。事件轮询连续不断地进行,所有待执行的任务被放入任务队列中。若任务队列中有多个任务,事件轮询则会保证其任务执行的顺序。但对于浏览器而言,它并不知道多个事件轮询队首的任务的优先级,因而它会更偏向于选择性能消耗更小的任务来执行(例如用户input框的输入行为)。那好,继续听我说……异步任务被安排了一个既定的顺序,因而浏览器能够能依据其内部机制为每个任务分配相关资源。保证每个任务依次有序执行。在异步任务执行期间,浏览器也可能有自身的页面的渲染更新。如一次点击事件触发时,也需要将其回调函数放入事件轮询任务队列中。其可能改变HTML结构等。又如上面所述的
setTimeout
。setTimeout
的作用是等待一个预先设定好的延时时间,然后把其回调函数推入任务队列中。为何setTimeout
会比script end
后输出,其原因便是script end
等同步语句是最优先任务的一种。setTimeout
则被放入一种单独的任务队列中。恩,这是现今我们都知道的。但是我需要你知道下面这一点……微任务通常会在当前同步代码执行之后立即执行,例如对处理一些
action
以及在不需要新开线程的情况下处理一些异步任务。只要没有其它的同步JavaScript
代码还在运行,在每个任务执行完毕后,微任务在回调函数之后便会排队形成微任务队列,在微任务排队期间任何其它微任务都会被添加到微任务队列队尾并且也将被处理。微任务包括Mutation Observer(变动观察器)以及上述所提到的Promise
回调。一旦一个
Promise
对象resolve
了,或者说它之前就已经resolve
了,为了避免它的回调函数出现错误调用,它会进入微任务队列。这样做保证了Promise
的回调函数是异步执行的(即使Promise
已经resolve
了)。因此我们通常把这个回调函数传入Promise.then()
而不是在在这个Promise
对象resolve
之后立刻执行下一个微任务。这便是promise1
和promise2
会在script end
之后打印的原因(由于立即执行的脚本会在微任务必须在微任务被处理之前执行完成)。而promise1
和promise2
会在setTimeout
之前被打印,这是由于微任务总会在下一段宏任务之前执行。因此,看下面这段代码的执行过程:
是的,我创建了一个动态图来展现这段代码的运行过程。你会怎样度过的你的礼拜六呢?是和你的朋友们出去晒太阳么?噢,我不会的。在这种情况下是不是该对我的UI设计功底刮目相看了呢,点击这个箭头看看。
一些浏览器的不同结果
一些浏览器会以这样的顺序打印:
script start
,script end
,setTimeout
,promise1
,promise2
。通常是在setTimeout
之后运行Promise
的回调函数。是把Promise
的回调作为一个新的宏任务而非微任务。这种情况是可解释的,由于
Promise
来源于ECMAScript
而不是HTML
。ECMAScript
拥有一个类似于microtasks
(微任务)的jobs
的概念。但是它与别的类型的任务的概念也不是太明确(参见vague mailing list discussions)。但是,在一般情况普遍认为Promise
是微任务的一种。若把
Promise
当做宏任务会导致性能问题,因为Promise
的回调函数也许会有不必要的延迟,导致宏任务队列的任务被阻塞,例如页面渲染。此外,由于它可能会依赖于其它任务的资源,因而可能造成死锁的产生。能够打破这类死锁的方式只有调用一些别的API,这是很蠢的。这是一篇
Microsoft Edge
的bug
修复日志。WebKit
早已使用正确的方式处理宏任务微任务问题。所以我也假想Safari
以后也会修复这一问题。此外,在Firefox 43
也是正确修复了这一问题的。十分有趣的是
Safari
与Firefox
在修复bug后都都经历过版本回退的事情,我也十分好奇以后它们是否会再出现这种问题。如何知道某类任务到底是宏任务还是微任务
测试是一种办法。我们通过和
Promise
和setTimeout
对比,观察它会在什么时候输出。正确的方法是,去查询
ECMAScript
标准和HTML
标准。例如setTimeout在宏任务中排列第14位,而Mutation Observer(变动观察器)在微任务中排列第5位。值得一提的是,在
ECMAScript
领域中,微任务(microtasks
)被称作为jobs
。PerformPromiseThen排列jobs
之中的第八位,EnqueueJob
也被称作一种微任务。现在,让我们看更多更复杂的例子。
更复杂的例子
在写这篇文章之前我把一个问题考虑错了,看这段
HTML
。执行下列这段JS代码,考虑在我点击
div.inner
之后会输出什么?在不同浏览器执行的结果是这样的:
哪个结果才是正确的?
调用
click
事件的回调函数其实也是一个宏任务,Mutation Observer(变动观察器)
和Promise
的回调函数都会被当做微任务进入微任务队列。setTimeout
的回调会在宏任务队列中排队。可到原文链接观察他们是如何执行的。因此
Chrome
中才能够得到正确的结果。有一种说法是微任务所在内存会在它的回调函数调用之后被释放,我认为它的内存其实会在宏任务队列执行完毕之后才被释放。这个规则可参照HTML
规范:此外微任务自身也会被核对是否正确进入了微任务队列,除非它已经被执行完毕。类似的,
ECMAScript
也对jobs
进行了描述。是什么让浏览器出现错误的结果
Firefox
和Safari
已经正确处理了多个点击事件之间的微任务队列,通过Mutation Observer(变动观察器)
的回调所显示。但是Promise
的打印时间却显得不同。这也是可解释的。在jobs
和微任务(microtasks)
之间的执行顺序是模糊的,但在不久的将来这些问题应该都会得到正确的解决办法。可参考Firefox ticket和Safari ticket在
Edge
中我们已经看到Promise
会以正确的顺序进入微任务队列中,但是却没有正确处理微任务队列和click
事件的顺序,相反微任务会在所有click
事件之后再开始执行。在两次click
打印之后便只有一次mutate
的打印。Bug ticket一个类似的例子
使用与上述相同的例子,在以下代码执行后会发生什么?
同样你可以到原文链接查看效果。
我发誓我是真的在
Chrome
中得到了不同的效果。我已经更新了这个图表很多次了。如果你在Chrome
中获得不同的测试结果,请在评论中告诉我你使用的是什么版本。为何测试结果会不同
因此正确的结果是:
click
,click
,promise
,mutate
,promise
,timeout
,timeout
,看起来似乎在Chrome
中得到了正确的结果。在标准里的每个事件监听回调是这样的:
在这之前,微任务会在事件回调之间运行。但是
click()
却会导致事件同步调度,因此调用click()
的脚本仍然在回调函数之间的堆栈中。上述规则确保微任务不会中断正在执行的JavaScript
代码。这意味着我们不会在事件监听回调之间处理微任务队列,而是在其之后进行处理。总结
JavaScript
代码在其中执行。