在说微任务与宏任务之前我们先说一下同步任务与异步任务的概念吧。
同步任务与异步任务
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
-
所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
-
主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,在"任务队列"之中放置一个事件。
-
一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
-
主线程不断重复上面的3。
以上摘自廖雪峰的博客 JavaScript 运行机制详解:再谈Event Loop.
问题
我们先看一下下面的代码,然后思考一下输出的先后顺序
setTimeout(() =>{
console.log('1')
});
new Promise((resolve) => {
console.log('2');
resolve();
}).then(() => {
console.log('3')
});
console.log('4');
按照同步与异步的概念来看,输出顺序应该是2、4、1、3.
但是,打开控制台,输入代码,查看输出,顺序是这样的2、4、3、1,发生了什么?
想知道发生了什么就继续往下看吧。
宏任务与微任务
除了广义的同步任务和异步任务,我们对任务有更精细的定义,分为宏任务和微任务。
- 宏任务:包括整体代码script,setTimeout,setInterval;
- 微任务:Promise,process.nextTick
注:Promise立即执行,then函数分发到“microtask”队列,process.nextTick分发到“microtask”队列
js引擎会把宏任务和微任务放置两个“任务队列”中,分别是“macrotask”队列以及“microtask”队列。在执行异步任务时,先执行宏任务,然后在执行微任务。
所以现在解释一下上面问题中的输出顺序问题:
- 这段代码作为宏任务,进入主线程
- 遇到setTimeout,把它的回掉函数放置“macrotask”队列中,然后接着执行下面的代码
- 遇到Promise,new Promise会立即执行,于是输出2,其then函数会被放置“microtask”队列
- 遇到console.log(‘4’)直接就执行了
- 整体代码script作为宏任务已经执行结束,判断“microtask”队列中是否有可执行的微任务(then函数),然后执行,输出3
- 至此,整个代码的第一轮循环结束了,要开始下一轮循环,先去查看“macrotask”队列,有setTimeout的回掉函数,然后执行,执行结束,输出1。
- 结束。
所以上述问题的输出顺序知道怎么肥死了吧?
盗一张事件循环,宏任务,微任务的关系图,如下:
图说明:进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到符合执行条件的一个宏任务执行完毕,再执行所有的微任务。
复习一下上面的知识点,我们瞅一眼以下代码:
console.log('1');
setTimeout(() => {
console.log('2');
process.nextTick(() => {
console.log('3');
})
setTimeout(() => {
console.log('10')
new Promise((resolve) => {
console.log('11');
resolve();
}).then(() => {
console.log('12')
})
})
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5')
})
})
process.nextTick(() => {
console.log('6');
})
new Promise((resolve) => {
console.log('7');
resolve();
}).then(() => {
console.log('8')
setTimeout(() => {
console.log('9')
})
})
console.log('10')
大声说出答案吧:1、7、10、6、8、2、4、3、5、9、10、11、12
好吧,我们分析一下:
- 整段代码作为宏任务,进入主线程
- 遇到console.log(‘1’),立即执行,并向下执行
- 遇到setTimeout,把它的回掉函数fn1,放置“macrotask”队列中,接着执行下面的代码
- 遇到process.nextTick,把其回调函数fn2放置“microtask”队列
- 遇到Promise,new Promise会立即执行,于是输出7,其then函数fn3会被放置“microtask”队列
- 遇到console.log(‘10’)直接就执行了
- 整体代码script作为宏任务已经执行结束,判断“microtask”队列中是否有可执行的微任务(fn2以及fn3),队列具体先进先出的特点,所以先执行fn2,输出6,然后执行fn3,输出8,里面包含setTimeout,把它的回调函数fn4放置“macrotask”队列中。
- 至此,整个代码的第一轮循环结束了,要开始下一轮循环。现在“macrotask”队列中有fn1、fn4
- 先去查看“macrotask”队列,先执行fn1。
- 执行fn1,遇到console.log(‘2’),就输出,遇到process.nextTick,将其回调函数fn5放置“microtask”队列,遇到setTimeout,把它的回掉函数fn6放置“macrotask”队列中,遇到Promise,new Promise会立即执行,于是输出4,其then函数fn7会被放置“microtask”队列,即这个宏任务执行完成。“macrotask”队列里面有fn4、fn6。
- 现在检查“microtask”队列,里面有fn5、fn7,把里面的任务全部执行完毕,
先执行fn5,输出3,再执行fn7,输出5 - 至此,又一轮的循环结束了
- 再检查“macrotask”队列,里面有fn4、fn6,执行fn4,输出9.
- 而现在的“microtask”队列是空的,再检查“macrotask”队列,有fn6
- 执行fn6,输出10,遇到new Promise,输出11,并把其回调函数fn8放置“microtask”队列,至此宏任务fn6结束,
- 检查“microtask”队列,并执行fn8,输出12,至此,“macrotask”队列以及“microtask”队列全部空了。
- 结束。