先看下面的代码
<script>
console.log(1);
setTimeout(function(){
console.log(2);
},0);
console.log(3);
</script>
上面的执行结果是1,3,2
原因:上面的setTimeout可以理解为异步函数调用,因为javascript是单线程的,主线程拥有一个执行栈和一个事件循环
当代码开始执行的时候,主线程会依次执行代码(就是script里面的代码),当遇到异步函数的时候(setTimeout),会将该函数加入到任务队列里面,然后继续执行,当主线程空闲后,然后把异步函数出栈,直到所有的异步函数执行完毕即可。
在一个浏览器环境中,只能有一个事件循环,但是可以有多个任务队列,而每个任务都有一个任务源,相同任务源的任务,只能放到一个任务队列中
上面的micro-task和macro-task就是两种不同的任务队列
macro-task:script(script标签里面的整体代码) setTimeout,setInterval,setImmediate,I/O,UI rendering
micro-task:process.nextTick,Promise,MutationObserver
具体流程
首先:全部代码(script)算一个macrotask.。
第一步:浏览器先执行一个macrotask;执行的过程中,创造了新的macrotask(setTimeout之类的),然后接着执行,把promise加入到micro-task队列里面
第二步:浏览器执行microtask(例如promise),这里会将microtask里面所有任务都取出
第三步:重复,浏览器会再执行一个macrotask
总的来说:macrotask每次只取一个,而microtask会一次取完
下面再来另外一个例子:
<script>
console.log(1);
setTimeout(function(){
console.log(2);
},0)
Promise.resolve().then(function(){
console.log(3);
}).then(function(){
console.log(4);
})
console.log(5)
</script>
上面的输出结果是 1,5,3,4,2
具体流程大概是下面这样:
当代码开始执行的时候,会先输出1,然后把setTimeout加入到一个macrotask 队列里面,接着把promise加入到microtask 队列里面,然后输出5
到这里我们代码执行完了一个macrotask(script里面的代码算第一个macrotask),接着要开始执行microtask,这次会把microtask里面所有的任务都执行完,这里就输出3和4,
当microtask执行完后,又会接着开始执行macrotask,就是setTimeout,到这里输出完2后,所有代码就都执行完毕
有个疑问 啊嘞嘞?是什么原因导致了原本应该在setTimeout回调后面的Promise的回调反而跑到前面去执行了呢?
为了搞清这个问题,我专门去翻阅了一下资料,首先找到了Promises/A+标准里面提到:
- 一个事件循环有一个或者多个任务队列;
- 每个事件循环都有一个microtask队列
- macrotask队列就是我们常说的任务队列,microtask队列不是任务队列
- 一个任务可以被放入到macrotask队列,也可以放入microtask队列
- 当一个任务被放入microtask或者macrotask队列后,准备工作就已经结束,这时候可以开始执行任务了。
可见,setTimeout和Promises不是同一类的任务,处理方式应该会有区别,具体的处理方式有什么不同呢?我从这篇文章里找到了下面这段话:
通俗的解释一下,microtasks的作用是用来调度应在当前执行的脚本执行结束后立即执行的任务。 例如响应事件、或者异步操作,以避免付出额外的一个task的费用。
microtask会在两种情况下执行:
1.任务队列(macrotask = task queue)回调后执行,前提条件是当前没有其他执行中的代码。
2.每个task末尾执行。
另外在处理microtask期间,如果有新添加的microtasks,也会被添加到队列的末尾并执行。
也就是说执行顺序是:
开始 -> 取task queue第一个task执行 -> 取microtask全部任务依次执行 -> 取task queue下一个任务执行 -> 再次取出microtask全部任务执行 -> ... 这样循环往复
Promise一旦状态置为完成态,便为其回调(.then内的函数)安排一个microtask。
接下来我们看回我们上面的代码
setTimeout(function(){
console.log(1)
},0);
new Promise(function(resolve){
console.log(2)
for( var i=100000 ; i>0 ; i-- ){
i==1 && resolve()
}
console.log(3)
}).then(function(){
console.log(4)
});
console.log(5);
按照上面的规则重新分析一遍:
1.当运行到setTimeout时,会把setTimeout的回调函数console.log(1)放到任务队列里去,然后继续向下执行。
2.接下来会遇到一个Promise。首先执行打印console.log(2),然后执行for循环,即时for循环要累加到10万,也是在执行栈里面,等待for循环执行完毕以后,将Promise的状态从fulfilled切换到resolve,随后把要执行的回调函数,也就是then里面的console.log(4)推到microtask里面去。接下来马上执行马上console.log(3)。
3.然后出Promise,还剩一个同步的console.log(5),直接打印。这样第一轮下来,已经依次打印了2,3,5。
4.现在第一轮任务队列已经执行完毕,没有正在执行的代码。符合上面讲的microtask执行条件,因此会将microtask中的任务优先执行,因此执行console.log(4)
5.最后还剩macrotask里的setTimeout放入的函数console.log(1)最后执行。
如此分析输出顺序是:
2
3
5
4
1
看吧,这次分析对了呢ヾ(◍°∇°◍)ノ゙
总结和参考资料
microtask和macrotask看起来容易混淆,实际上还是很好区分的。macrotask就是我们常说的任务队列(task queue)。
JavaScript执行顺序可以简要总结如下:
开始 -> 取task queue第一个task执行 -> 取microtask全部任务依次执行 -> 取task queue下一个任务执行 -> 再次取出microtask全部任务执行 -> ...
循环往复,直至两个队列全部任务执行完毕。
Tasks, microtasks, queues and schedules
github-setImmediate.js
知乎-Promise的队列与setTimeout的队列有何关联?
阮一峰-JavaScript 运行机制详解:再谈Event Loop
百度;