前言
我相信即将参加各类面试的大家一定都对 EventLoop 有一定程度的了解,本文主要针对如何应试的面对各类 EventLoop 的执行过程面试题,其中很多内容是作者自己经过多次试验得出的,可能不完全权威科学,但能帮助一些需要快速应对面试的小伙伴们直观地解决这一问题。
1. EventLoop 执行顺序
在 js 中,任务分为宏任务(macrotask)和微任务(microtask)。
在常见的问题中,宏任务中包含同步代码 script 和异步中的 setTimeout setInterval setImmediate,微任务包含 Promise.then。
2. 多个Promise.then的执行顺序
简单来说,Promise.then首先根据进入队列的顺序判断先后顺序,然后根据同一队列中执行快慢判断先后顺序。(还是通过举例说明更能明白这个规律。)
console.log('start');
let p = Promise.resolve('baz')
let p2 = p.then((res)=>{
console.log('res');
return new Promise(resolve =>resolve(res))
})
p2.then(a => console.log(a) )
const promise = new Promise((resolve) => {
console.log('bar1');
resolve('bac')
console.log('bar2');
})
let promise2 = promise.then(res =>{
console.log(res);
return res
})
promise2.then(()=>console.log('aaa'))
看上去很复杂,但我们分析一下就清楚了:
1. 执行同步代码 console.log('start'),打印 start
2. Promise.resolve('baz') 是实例化过程,也是同步执行(此时 p 状态为 fulfilled)
3. 将 p.then 放入微任务队列
4. 将 p2.then 放入微任务队列
5. new Promise() 是实例化过程,同步执行,打印 bar1 -> 更改promise 状态为 fulfilled -> 打印 bar2
6. 将 promise.then 放入微任务队列
7.将 promise2.then 放入微任务队列
此时微任务队列中有四个任务,可以思考一下这四个任务的执行顺序:
任务 | p.then | p2.then | promise.then | promise2.then |
打印结果 | res | baz | bac | aaa |
答案是:res -> bac -> aaa -> baz
这很明显不完全是按照放入顺序执行的,那我们还要关注什么地方呢?
Promise.then 中 Promise 的状态变更顺序。
Promise 的状态决定 Promise.then 的执行顺序,若状态为 pending,执行靠后;若状态为 fulfilled ,执行靠前。(两种相对而言)
那么我们就可以根据这点进行判断了:p 与 promise 的状态都为 fulfilled ,而 p2 与 promise2 的状态都为 pending ,因此先执行 p.then 和 promise.then 。那么这两者之间有先后顺序吗?实际上他们都是通过 resolve() 函数改变的状态,没有进一步的先后区别,因此根据放入队列的顺序执行。
现在队列中就剩下两个任务了,他们之间的顺序该如何判断呢?首先我们知道在 p.then 和 promise.then 执行之前 p2 与 promise2 的状态都为 pending 。但当这两个微任务执行后,p2 与 promise2 的状态就变为了 fulfilled,那么这两者之间有先后顺序吗?这里就和上面两者不同了,他们是有先后顺序。promise2 通过返回 非 Promise 对象 改变状态成 fulfilled,p2 通过返回 Promise 对象 改变状态成 fulfilled,前者的执行时间会快于后者。
若 .then 返回一个非 Promise 对象,其执行时间(变更状态的时间)会快于返回一个 Promise 对象。
这样大家能明白为什么答案是:res -> bac -> aaa -> baz 了吗?
3. async/await 执行顺序
还是先总结一下:
1. await 只能写在 async 修饰的函数中;
2. 执行完 await 修饰的对象后,先执行 async 外的同步代码,然后再回到原处继续执行。
(还是通过举例说明)
async function test1(){
console.log('start test1');
console.log(await test2());
console.log('end test1');
}
async function test2(){
console.log('test2');
return 'return test2 value'
}
test1();
console.log('start async');
setTimeout(()=>{
console.log('setTimeout');
},0);
new Promise((resolve,reject)=>{
console.log('promise1');
resolve();
}).then(()=>{
console.log('promise2');
})
分析如下:
1. 执行同步代码 test1(),进入到test1() 代码中,打印 start test1
2. 遇到 await 函数test2,先进入test2()代码中,打印 test2,返回值 'return test2 value'。此时 await 执行完毕,跳出这个 async 函数执行同步代码。
3.执行同步代码 console.log('start async'),打印 start async
4. 将 setTimeout 放入宏任务队列
5. new Promise() 是实例化过程,同步执行,打印 promise1,然后更改 Promise 状态为 fulfilled
6. 将 Promise.then 放入微任务队列。此时同步代码执行完毕,重新回到 await 执行完毕的地方。
7. 此时 console.log(await test2()) 中已变成了console.log('return test2 value'),打印 return test2 value
8. 执行同步代码 console.log('end test1'),打印 end test1
9. 此时所有同步代码执行完毕,查找微任务队列:执行 Promise.then,打印 promise2
10. 此时微任务队列为空,查找宏任务队列:执行 setTimeout,打印 setTimeout
遇到 await 就阻塞,执行完 async 外面的同步代码后,再回到内部。
好了,我们再来看看这道网易的笔试题:
(async ()=>{
console.log(1);
setTimeout(()=>{
console.log(2);
},0)
await new Promise((resolve,reject)=>{
console.log(3);
resolve()
}).then(()=>{
console.log(4);
})
console.log(5);
})()
// 1 3 4 5 2
分析如下:
1. 执行同步代码 console.log(1),打印 1
2. 将 setTimeout 放入宏任务队列
3. 遇到 await 修饰的 new Promise 函数,执行内部所有代码:执行同步代码打印 3 -> 更改 Promise 状态为 fulfilled -> 执行.then 打印 4
4. 执行完await 修饰的内部代码,跳出 async 执行同步代码(此处无)
5. 返回内部继续执行剩余同步代码,打印 5
6. 此时所有同步代码执行完毕,微任务队列为空,查找宏任务队列:执行 setTimeout,打印 2
可能大家疑惑的点在于为什么遇到 .then 这项微任务,也要一起执行呢?
await new Promise((resolve,reject)=>{
console.log(3);
resolve()
}).then(()=>{
console.log(4);
})
其实,上述代码实际上可以拆分为下面这样:
let p = new Promise((resolve,reject)=>{
console.log(3);
resolve(4)
})
await p.then((res)=> {
console.log(res)
})
有些读者看了可能会说,为什么不是 await 修饰 p 呢?也就是下面这样:
let p = await new Promise((resolve,reject)=>{
console.log(3);
resolve(4)
})
p.then((res)=> {
console.log(res)
})
实际上这样写代码是一定报错的。因为 await 修饰的对象只会返回一个非 Promise 对象,而非 Promise 对象是不可能可以用 .then 的。
await 修饰的对象是 Promise 和 不是 Promise 的区别在于前者将 Promise 的结果值(即[[PromiseResult]]) 作为 await 结果,后者将 对象本身 作为 await 结果。
总而言之,await 结果只有可能是非 Promise 对象。因此我们也可以知道,await 若是修饰 Promise 对象,而该对象使用一个甚至多个 .then,实际上 await 修饰的是最后一个 .then 。