setTimeout(() => {
console.log('timeout')
})
new Promise((resolve) => {
console.log('before')
resovle('then')
console.log('after')
}).then(res => {
console.log(res)
})
这段代码的执行结果为 before -> after -> then -> timeout 。
不要怀疑,这是就是正确答案。
此时你是不是满脑袋的问号呢,如果是,那就请客官您继续往下看,看完本文,您就会觉得原来这段代码只不过是 a piece of cake。
重点知识来啦:
什么是宏任务(Macrotasks)和微任务(Microtasks)
首先我们应该知道JavaScript 是单线程的脚本语言(在执行某一段代码的时候是不会同时执行另一段代码的)。
如果所有的代码都同步执行的话会有什么结果呢,比如当程序向后台发送了一个请求,此时你能做的只有双手合十祈祷网速快一点,因为程序只有在得到后台回应的时候才会执行其他的操作。
于是就出现了异步事件的概念,当程序向后台发送一个请求以后,可以做一些其他的事,即使请求返回的数据程序已经拿到了,也需要等到程序做完正在做的事,才会处理请求返回的数据,这就是异步事件。
而异步任务又分为宏任务和微任务,其中微任务的优先级高于宏任务。
宏任务有: setTimeout, setInterval, setImmediate(node中),requestAnimationFrame(浏览器);
微任务有:process.nextTick(node中), MutationObserver(浏览器),catch, finally, Promise.then。
其中需要注意的是Promise中的函数遇到立即执行,只有promise所对应的then中执行的代码片段需要加入‘微任务’队列(队列遵循先进先出原则)。
总结经验就是:全局的js语句与promise中的语句顺序执行(遇到就执行),而宏任务遇到需放入宏任务队列,遇到微任务则放入微任务队列,当前代码溜了一遍之后(之所以说溜了一遍是因为并不是严格意义上的代码执行完毕了),再将微任务中的代码块按顺序执行,当执行完队列中的所有微任务,就进入宏任务队列依次执行宏任务代码块。
对于喜欢看图说话的人,那我就把网上随处可见的图搬过来送你们啦(不用谢^_^)。
既然有了理论的概念,下面就通过例子来检测你有没有掌握(就以最常见的Promise和setTimeout来做实例)
目前你正处于小白阶段,赶快通过磨练来晋级吧。
实例验证
1. 单个的宏任务或微任务
console.log('start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
console.log('end');
输出答案为:start -> end -> setTimeout
解析: 普通js代码遇到就直接执行,宏任务需先放入宏任务队列,到当前环境代码溜一遍之后发现微任务队列里面没有需要执行的代码块,则执行宏任务。
console.log('start');
new Promise(resolve => {
console.log('promise');
resolve('then');
}).then((data) => {
console.log(data);
})
console.log('end');
输出答案为:start -> promise -> end -> then
解析:普通js代码遇到就直接执行,微任务需先放入微任务队列,到当前环境代码溜一遍之后执行微任务中的代码,然后发现宏任务队列里面没有代码块需要执行,则整个代码执行完毕。
如果你上面的两个题都做对了,那恭喜你已经将理论与实践结合起来了,但是想要进一步理解,需要通过下一个题目来检验。
当然如果你没有回答正确请不要灰心,可以喝口水缓缓,然后从头再梳理一遍。
2. 宏任务和微任务混搭:
console.log('start');
new Promise((resolve) => {
console.log('promise1')
resolve('then1')
console.log('promise1 end')
}).then(res => {
console.log(res)
})
console.log('middle');
setTimeout(() => {
console.log('timeout1')
})
new Promise((resolve) => {
console.log('promise2')
resolve('then2')
console.log('promise2 end')
}).then(res => {
console.log(res)
});
console.log('end');
输出答案为:start -> promise1 -> promise1 end -> middle -> promise2 -> promise2 end -> end -> then1 -> then2 -> timeout1。
解析:先输出start,然后输出promise1,再输出promise1 end 我就得你应该已经明白为什么了,那我们接下来从这里开始分析,此时应该在微任务队列中放入 then第一个promise对应的then语句;遇到middle,直接执行,timeout 放入宏任务队列;遇到第二个promise,直接执行promise中的语句,然后将对应的then中的代码块放入微任务队列,成为微任务2号;在执行end。到目前为止,所有的代码都溜了一遍了,下面先依次执行微任务列表中的代码块,为then1,then2,最后执行宏任务timeout1。
怎么样,你有没有get到关键所在呢,接下来就要放大招了,你准备好了吗?
3.每个promise对应多个then方法
console.log('start');
setTimeout(() => {
console.log('timeout1')
}, 0);
new Promise((resolve, reject) => {
console.log('promise1 start');
resolve('promise1 end');
}).then((data) => {
console.log(data, 1);
return new Promise(resolve => {
console.log('promise3');
resolve(data);
})
}).then(data => {
console.log(data, 2)
});
new Promise((resolve, reject) => {
console.log('promise2 start');
resolve('promise2 end');
}).then((data) => {
console.log(data, 1);
return new Promise(resolve => {
console.log('promise4');
resolve(data);
})
}).then(data => {
console.log(data, 2)
});
console.log('finish');
输出答案:start -> promise1 start -> promise2 start -> finish -> promise1 end 1 -> promise3 -> promise2 end 1 -> promise4 -> promise1 end 2 -> promise2 end 2 -> timeout1
解析:具体运行步骤如下:
① 执行一遍全局代码:
start: 输出start -> timeout1: 将timeout1放入宏任务队列 -> promise1: 输出promise1 start -> then1.1: 将then1.1放入微任务队列 -> promise2: 输出promise2 start -> then2.1: 将then2.1放入微任务队列 -> finish:输出finish
执行完毕时,各队列及输出如下:
宏任务队列:[ 'timeout1' ]
微任务队列: [ 'then1.1', 'then2.1' ]
输出:start -> promise1 start -> promise2 start -> finish
② 执行微任务队列中的第一位:
执行then1.1:输出 promise1 end 1 -> promise3: 输出promise3-> then1.2: 将then1.2 放入微任务队列
执行完毕时,各队列及输出如下:
宏任务队列:[ 'timeout1' ]
微任务队列: [ 'then2.1', 'then1.2' ]
输出:start -> promise1 start -> promise2 start -> finish -> promise1 end 1 -> promise3
③ 执行微任务队列中的第一位:
执行then2.1:输出 promise2 end 1 -> promise4: 输出promise4 -> then2.2: 将then2.2 放入微任务队列
执行完毕时,各队列及输出如下:
宏任务队列:[ 'timeout1' ]
微任务队列: [ 'then1.2', 'then2.2' ]
输出:start -> promise1 start -> promise2 start -> finish -> promise1 end 1 -> promise3 -> promise2 end 1 -> promise4
④ 执行微任务队列中的第一位:
执行then1.2:直接输出 promise1 end 2
执行完毕时,各队列及输出如下:
宏任务队列:[ 'timeout1' ]
微任务队列: [ 'then2.2' ]
输出:start -> promise1 start -> promise2 start -> finish -> promise1 end 1 -> promise3 -> promise2 end 1 -> promise4 -> promise1 end 2
⑤ 执行微任务队列中的第一位:
执行then2.2:直接输出 promise2 end 2
执行完毕时,各队列及输出如下:
宏任务队列:[ 'timeout1' ]
微任务队列: []
输出:start -> promise1 start -> promise2 start -> finish -> promise1 end 1 -> promise3 -> promise2 end 1 -> promise4 -> promise1 end 2 -> promise2 end 2
⑥ 检查微任务队列:
此时微任务队列为空,则进行下一步
⑦ 执行宏任务队列中的第一位:
执行timeout1: 直接输出timeout1
执行完毕时,各队列及输出如下:
宏任务队列:[]
微任务队列: []
输出:start -> promise1 start -> promise2 start -> finish -> promise1 end 1 -> promise3 -> promise2 end 1 -> promise4 -> promise1 end 2 -> promise2 end 2 -> timeout1
此时,整段代码执行完毕,最终输出结果如下:
start -> promise1 start -> promise2 start -> finish -> promise1 end 1 -> promise3 -> promise2 end 1 -> promise4 -> promise1 end 2 -> promise2 end 2 -> timeout1
答对了不要高兴得太早呀,终极大挑战等着你呢!come on
4. 宏任务与微任务嵌套
// 我为所有的语句标了代号,以便后面解析使用
// timeout1
setTimeout(() => console.log('timeout1'), 0);
// timeout2
setTimeout(() => {
console.log('timeout2');
// promise1
new Promise(resolve => {
console.log('promise1');
resolve();
// then1
}).then(() => {
console.log('then1');
// promise2
new Promise(resolve => {
console.log('promise2');
resolve();
// then2
}).then(() => {
console.log('then2');
})
// here
console.log('here');
})
// timeout3
setTimeout(() => console.log('timeout3'), 0);
}, 0);
// timeout4
setTimeout(() => console.log('timeout4'), 0);
// promise3
new Promise(resolve => {
console.log('promise3');
resolve();
// then3
}).then(() => {
console.log('then3');
})
输出答案为:promise3 -> then3 -> timeout1 -> timeout2 -> promise1 -> then1 -> promise2 -> here -> then2 -> timeout4 -> timeout3
解析:具体执行步骤如下:
① 执行一遍整体代码:
执行过程:
timeout1: 入宏任务队列 -> timeout2: 入宏任务队列 -> timeout4入宏任务队列 -> promise3:输出‘promise3’ -> then3: 入微任务队列。
此时各队列和输出如下:
宏任务队列:[ 'timeout1', 'timeout2', 'timeout4' ]
微任务队列:['then3']
输出:promise3
② 执行当前微任务队列:
then3:直接输出then3
执行完毕时:
宏任务队列:[ 'timeout1', 'timeout2', 'timeout4' ]
微任务队列:[]
输出:promise3 -> then3
③ 执行宏任务队列排在第一位的代码块:
宏任务队列中排在第一位的为timeout1,比较简单,直接输出timeout1就好。
执行完毕时:
宏任务队列:[ 'timeout2', 'timeout4' ]
微任务队列:[]
输出:promise3 -> then3 -> timeout1
④ 检查当前微任务队列:
当前微任务队列为空,则继续执行宏任务队列中第一位的代码块。
⑤ 执行宏任务队列排在第一位的代码块(其实就是重复上面的步骤,但是过程中需要谨慎)
执行一遍宏任务队列中排在第一位的代码块timeout2,过程如下:
输出timeout2 -> promise1: 输出promise1-> then1: 放入微任务队列 -> timeout3:放入宏任务队列
此时:
宏任务队列:['timeout4', 'timeout3']
微任务队列:[ 'then1' ]
输出:promise3 -> then3 -> timeout1 -> timeout2 -> promise1
⑥ 执行当前微任务队列中的代码块
执行then1过程如下:
输出then1-> promise2:输出promise2 -> then2: 放入微任务队列 -> here: 输出here
执行完毕时:
宏任务队列:[ 'timeout4', 'timeout3']
微任务队列:['then2']
输出:promise3 -> then3 -> timeout1 -> timeout2 -> promise1 -> then1 -> promise2 -> here
⑦ 执行微任务队列的代码块
执行then2: 直接输出then2
此时:
宏任务队列:['timeout4', 'timeout3']
微任务队列:[]
输出:promise3 -> then3 -> timeout1 -> timeout2 -> promise1 -> then1 -> promise2 -> here -> then2
⑧ 检查微任务队列
此时微任务队列中没有需要执行的代码块,进行下一步,执行宏任务队列中的第一位
⑨ 执行宏任务队列中排在第一位的代码块
执行timeout4:直接输出timeout4
此时:
宏任务队列:['timeout3']
微任务队列:[]
输出:promise3 -> then3 -> timeout1 -> timeout2 -> promise1 -> then1 -> promise2 -> here -> then2 -> timeout4
⑩ 检查微任务队列
此时微任务队列中没有需要执行的代码块,进行下一步,执行宏任务队列中的第一位
⑪ 执行宏任务队列中排在第一位的代码块
执行timeout3:直接输出timeout3
任务队列中排在第一位的代码块
执行timeout3:直接输出timeout3
此时:
宏任务队列: []
微任务队列:[]
输出:promise3 -> then3 -> timeout1 -> timeout2 -> promise1 -> then1 -> promise2 -> here -> then2 -> timeout4 -> timeout3
至此,整个程序完执行完毕,因此最终输出为:
promise3 -> then3 -> timeout1 -> timeout2 -> promise1 -> then1 -> promise2 -> here -> then2 -> timeout4 -> timeout3
上面的解析有点详细,但是对于做错的人看起来会比较方便一点。
如果你上面的都回答正确,那么恭喜你,可以去迎接工作中过关斩将了。