从事件循环看Promise链与自引用导致的阻塞问题

215 阅读4分钟

事情的起因

今天我的大学学长和我们讲了一道美团的事件循环机制的题目,他说面试的时候很多人都没答上来。面试后,和一起面试的人交流都没有讨论出原因,今天问了下老师才梳理出了原因,首先我把题目掏出来看下。

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会打印

  1. 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); // 永远不会执行
    });
    
  2. 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 的变量提升和赋值时机:

  1. var a 的变量提升:

    • 代码开始时 a 被声明但未赋值,值为 undefined
  2. a = new Promise(...) 的执行时机:

    • 构造函数参数中的 async 函数会立即执行,但此时 a 的赋值尚未完成!
    • 当执行到 console.log(a) 时,a 仍处于赋值过程中,值为 undefined
  3. 为什么 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

image.png

事件循环流程分解

阶段 1:同步代码执行

  1. 声明变量 a

    • var a 变量提升,初始值为 undefined
  2. 执行 var b = new Promise(...)

    • 同步执行 Promise 构造函数

      • 输出 1 
      • 设置 setTimeout(() => resolve(2), 1000)(这是一个 宏任务,1秒后加入队列)。
  3. 执行 a = new Promise(...)

    • 同步执行构造函数中的 async 函数

      • console.log(a):此时 a 尚未被赋值,输出 undefined 
      • await b:暂停执行,将后续代码包装为 微任务(记为微任务1) ,等待 b 完成。
  4. 执行 console.log("end")

    • 输出 end 

阶段 2:处理微任务队列(此时为空)

  • 当前没有可执行的微任务(b 尚未完成,微任务1 仍在等待)。

阶段 3:执行宏任务(1秒后)

  1. setTimeout 回调触发

    • 执行 resolve(2),将 b 的状态变为 fulfilled
    • 触发 b.then(() => console.log(3)),将它的回调加入 微任务队列(记为微任务2)

阶段 4:处理微任务队列

  1. 执行微任务2

    • 输出 3 
    • 触发下一个 .then(() => console.log(4)),加入微任务队列(记为微任务3)。
  2. 执行微任务3

    • 输出 4 
    • 触发下一个 .then(() => console.log(5)),加入微任务队列(记为微任务4)。
  3. 执行微任务4

    • 输出 5 
  4. 执行微任务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 的构造函数恢复后输出