事情的起因
今天我的大学学长和我们讲了一道美团的事件循环机制的题目,他说面试的时候很多人都没答上来。面试后,和一起面试的人交流都没有讨论出原因,今天问了下老师才梳理出了原因,首先我把题目掏出来看下。
var a
var b = new Promise((resolve) => {
console.log(1);
setTimeout(() => {
resolve(2)
}, 1000)
}).then(() => {
console.log(3);
}).then(() => {
console.log(4);
}).then(() => {
console.log(5);
})
a = new Promise(async (resolve) => {
console.log(a)
await b
console.log(a)
await (a)
resolve(true)
console.log(6);
})
console.log('end');
我没能写出的原因
1.不知道await a 导致的死锁,认为6会打印
-
a的初始状态
当执行a = new Promise(...)时,构造函数中的async函数会立即执行:javascript
a = new Promise(async (resolve) => { console.log(a); // 此时 a 尚未被赋值,输出 undefined await b; // 等待 b 完成(1秒后) console.log(a); // 此时 a 已被赋值为 Promise(状态 pending) await a; // 等待 a 自己(死锁) resolve(true); // 永远不会执行 console.log(6); // 永远不会执行 }); -
await a的行为await a会等待a的状态变为 fulfilled 或 rejected。- 但此时
a的resolve(true)在await a之后,而a的状态尚未改变(仍为 pending)。 - 因此,
await a会永远等待,形成一个死锁,后续代码(包括resolve(true)和console.log(6))永远无法执行。(2)后,b的状态改变,await b可以向后执行了。
2.没有注意到var 的变量提升和赋值时机:
-
var a的变量提升:- 代码开始时
a被声明但未赋值,值为undefined。
- 代码开始时
-
a = new Promise(...)的执行时机:- 构造函数参数中的 async 函数会立即执行,但此时
a的赋值尚未完成! - 当执行到
console.log(a)时,a仍处于赋值过程中,值为undefined。
- 构造函数参数中的 async 函数会立即执行,但此时
-
为什么
console.log(a)输出undefined?- JavaScript 的赋值操作是“先创建对象,后赋值变量”。
- 但在构造函数内部,
a的赋值操作尚未完成(构造函数本身正在执行),因此a仍为undefined。
对代码的解释
var a; // 变量提升,此时 a = undefined
var b = new Promise((resolve) => {
console.log(1); // 同步输出 1
setTimeout(() => resolve(2), 1000); // 宏任务(1秒后)
}).then(() => console.log(3)) // 微任务
.then(() => console.log(4)) // 微任务
.then(() => console.log(5)); // 微任务
a = new Promise(async (resolve) => { // 注意:构造函数参数是 async 函数
console.log(a); // 此时 a 尚未被完全赋值!输出 undefined
await b; // 等待 b 完成
console.log(a); // 此时 a 已被赋值为 Promise(状态 pending)
await a; // 等待自己(死锁)
resolve(true); // 永远不会执行
console.log(6); // 永远不会执行
});
console.log("end"); // 同步输出 end
事件循环流程分解
阶段 1:同步代码执行
-
声明变量
avar a变量提升,初始值为undefined。
-
执行
var b = new Promise(...)-
同步执行 Promise 构造函数:
- 输出
1 - 设置
setTimeout(() => resolve(2), 1000)(这是一个 宏任务,1秒后加入队列)。
- 输出
-
-
执行
a = new Promise(...)-
同步执行构造函数中的
async函数:console.log(a):此时a尚未被赋值,输出undefinedawait b:暂停执行,将后续代码包装为 微任务(记为微任务1) ,等待b完成。
-
-
执行
console.log("end")- 输出
end
- 输出
阶段 2:处理微任务队列(此时为空)
- 当前没有可执行的微任务(
b尚未完成,微任务1仍在等待)。
阶段 3:执行宏任务(1秒后)
-
setTimeout回调触发- 执行
resolve(2),将b的状态变为 fulfilled。 - 触发
b.then(() => console.log(3)),将它的回调加入 微任务队列(记为微任务2) 。
- 执行
阶段 4:处理微任务队列
-
执行微任务2
- 输出
3 - 触发下一个
.then(() => console.log(4)),加入微任务队列(记为微任务3)。
- 输出
-
执行微任务3
- 输出
4 - 触发下一个
.then(() => console.log(5)),加入微任务队列(记为微任务4)。
- 输出
-
执行微任务4
- 输出
5
- 输出
-
执行微任务1(之前暂停的
a的构造函数)-
恢复
await b后的代码:console.log(a):此时a已被赋值为 Promise(状态为 pending),输出Promise { <pending> }await a:等待a自身变为 fulfilled,将后续代码包装为 微任务(记为微任务5) ,但此时a仍为 pending,因此微任务5 被阻塞,无法加入队列。
-
阶段 5:后续事件循环
- 微任务5 永远不会被加入队列,因为
a的状态始终为 pending(resolve(true)被await a阻塞)。 - 事件循环发现没有其他任务(微任务和宏任务队列均为空),结束运行。
执行结果
1 // 同步代码
undefined // a 尚未赋值
end // 同步代码
3 // b.then 链
4 // b.then 链
5 // b.then 链
Promise { <pending> } // a 的构造函数恢复后输出