之前这篇文章介绍回调函数的时候提到了函数调用栈和任务队列的概念,当时并没有深入探讨任务队列这个概念,只提到回调函数是推入任务队列中,待到当前函数调用栈为空时事件循环将执行任务队列中的下一个任务。
今天无意间看到了这篇文章介绍了tasks和microtasks的概念,谷歌大牛果然不同,幽默风趣,非常值得一读!本文基本是由该文章翻译过来,加入些许个人的理解。
先瞅一个例子:
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
跟你想的一样嘛?为什么会是这样的结果?听我细细道来。
正如前面的文章所提到,js是单线程的,每个线程有它自己的唯一的事件循环,事件循环的任务源可以不唯一。类似setTimeout, promise, ajax, DOM操作等都是典型的任务源,任务队列中的任务便是来自这些任务源。而这些任务源产生的任务又可以分为Tasks和Microtasks两种。
tasks
tasks中的任务都是有时间顺序的,因此浏览器能够有序地从中调度任务并执行。在任务与任务之间,浏览器可能会渲染更新。
tasks中一个典型就是setTimeout,setTimeout函数等待给定的延迟事件然后将其回调函数推入Tasks中。这就是为什么先输出'script end' 后输出'setTimeout'的原因。
tasks主要有:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
microtasks
microtasks中的任务在当前函数调用栈中的函数执行完成之后即调度,像promise、mutation都会被推入microtasks队列中。并且microtasks中的一个任务执行完成后,后续的microtask也会继续执行,直到Microtasks为空,这就解释了为什么promise2也会在setTimeout之前输出的原因。
micro-tasks主要有process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)
当tasks队列中的一个任务时,如果函数调用栈为空,便会开始执行microtasks中的任务,直至microtasks中所有任务执行完毕,然后event loop才会继续执行tasks中的下一个任务。
准备好接受一个更加复杂的例子嘛?Let's go!
<div class='outer'>
<div class='inner'></div>
</div>
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
//Listen for attribute changes on the outer element
new MutationObserver(function() {
console.log('mutate')
}).observe(outer, {
attributes: true
});
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
outer.setAttribute('data-random', Math.random());
}
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
输出结果如下:
click
promise
mutate
click
promise
mutate
timeout
timeout
上述例子中Dispatch click和setTimeout属于tasks,对应的回调函数被推进tasks队列中,Mutation observer和promise属于Microtasks,对应的回调函数则被推入Microtasks队列中。当点击inner元素时,代码执行执行过程如下所示
-
Dispatch click被推入tasks中,当点击inner元素时onClick被推入函数调用栈中,执行上下文进入onClick中,将setTimeout的回调函数推入tasks队列中,Mutation observers和Promise then的回调函数推入microtasks队列中,并执行输出click。
1.jpg
2.当onClick执行结束后, 函数调用栈为空,将promise then 的回调函数推入函数调用栈
2.jpg
-
同样地,当promise then的回调函数执行结束后,将mutation observers的回调函数推入函数调用栈
绘图3.jpg -
由于事件冒泡机制,父元素outer也会响应点击事件,因此重复1-3步骤,执行结束后如下所示
绘图4.jpg -
此时函数调用栈和microtasks中均为空,因此event loop将执行tasks中的下一个任务
绘图5.jpg -
再执行tasks中的最后一个任务
绘图6.jpg
整个过程大致如上所示,通过这个梨子,可以比较清晰地掌握event loop调用任务的顺序。
以上例子均在谷歌浏览器中的输出结果,不同浏览器有所差异,以谷歌浏览器为准!
网友评论