先看HTML标准的一系列解释:
现在我们知道了浏览器运行时有一个叫事件循环的机制。
总结一下,一个事件循环里有很多个任务队列(task queues)来自不同任务源,每一个任务队列里的任务是严格按照先进先出的顺序执行的,但是不同任务队列的任务的执行顺序是不确定的。按我的理解就是,浏览器会自己调度不同任务队列。网上很多文章会提到macrotask
这个概念,其实就是指代了标准里阐述的task
。
标准同时还提到了microtask
的概念,也就是微任务。看一下标准阐述的事件循环的进程模型:
执行进入microtask检查点时,用户代理会执行以下步骤:
现在我们知道了。在事件循环中,用户代理会不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask队列是否为空(执行完一个task的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去task队列中取下一个task执行...
那么哪些行为属于task或者microtask呢?标准没有阐述,但各种技术文章总结都如下:
macrotasks
: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI renderingmicrotasks
: process.nextTick, Promises, Object.observe(废弃), MutationObserver
来看一个例子:
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');
(代码来自Tasks, microtasks, queues and schedules,推荐观看原文的代码可视化执行步骤)
如果你测试的浏览器支持的Promise不支持Promise/A+标准,或是你使用了其他Promise polyfill,运行结果可能有差异。
运行结果是:
script start
script end
promise1
promise2
setTimeout
解释一下过程。
- 一开始task队列中只有script,则script中所有函数放入函数执行栈执行,代码按顺序执行。
接着遇到了setTimeout
,它的作用是0ms后将回调函数放入task队列中,也就是说这个函数将在下一个事件循环中执行(注意这时候setTimeout执行完毕就返回了)。
- 接着遇到了
Promise
,按照前面所述Promise属于microtask,所以第一个.then()会放入microtask队列。 - 当所有script代码执行完毕后,此时函数执行栈为空。开始检查microtask队列,此时队列不为空,执行.then()的回调函数输出'promise1',由于.then()返回的依然是promise,所以第二个.then()会放入microtask队列继续执行,输出'promise2'。
- 此时microtask队列为空了,进入下一个事件循环,检查task队列发现了setTimeout的回调函数,立即执行回调函数输出'setTimeout',代码执行完毕。
继续看一个更有趣的例子:
HTML代码:
<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);
(代码来自Tasks, microtasks, queues and schedules,推荐观看原文的代码可视化执行步骤)
点击内框后,结果如下:
click
promise
mutate
click
promise
mutate
timeout
timeout
解释一下过程:
点击inner输出'click',Promise和设置outer属性会依次把Promise和MutationObserver推入microtask队列,setTimeout则会推入task队列。此时执行栈为空,虽然后面还有冒泡触发,但是此时microtask队列会先执行,所以依次输入'promise'和'mutate'。接下来事件冒泡再次触发事件,过程和开始一样。接着代码执行完毕,此时进入下一次事件循环,执行task队列中的任务,输出两个'timeout'。
好了,如果你理解了这个,那么现在换一下事件触发的方式。在上面的代码后面加上
inner.click()
思考看看会有什么不同。
运行结果:
click
click
promise
mutate
promise
timeout
timeout
造成这个差异的结果是什么呢?因为第一次执行完第一个click事件后函数执行栈并不为空。
具体代码运行解释,可以查看Tasks, microtasks, queues and schedules。
本文参考:
html.spec.whatwg.org
difference-between-javascript-macrotask-and-microtask
Event loop
墙裂建议大家阅读HTML标准里阐述的Event Loop,欢迎指正和建议。