大模型也栽跟头的 Promise 题!来挑战一下?

435 阅读5分钟

🌟 开场白:Promise,你以为的它,真的是它吗?

各位掘友,大家好!

在前端的武林中,Promise 绝对是内功心法级别的存在。我们每天都在用 .then()、.catch(),用它来处理异步,但你有没有遇到过那种让你直呼“卧槽,这顺序不对啊!”的 Promise 题?

今天,我们就来挑战一道看似简单,实则暗藏玄机的 Promise 经典面试题。它能完美地考察你对 JavaScript 事件循环、微任务队列,尤其是 Promise 状态吸收(Promise Resolution Procedure) 的理解。

如果你能准确说出下面这段代码的输出顺序,恭喜你,你的 Promise 功力至少是“内功小成”!


⚠️ 灵魂拷问面试题:输出结果是多少?

请看这段代码,并思考一下,1, 2, 3, 4, 5, 6, 7 这几个数字的打印顺序会是怎样的?

const p1 = Promise.resolve();

const p2 = new Promise((resolve) => {
  // 关键点:用 p1 来 resolve p2
  resolve(p1);
})

console.log(p1)
console.log(p2)

// p2 链
p2.then(()=>{
  console.log(1);
})
  .then(()=>{
    console.log(2);
  })
  .then(()=>{
    console.log(3);
  });

// p1 链
p1.then(()=>{
  console.log(4);
})
  .then(()=>{
    console.log(5);
  })
  .then(()=>{
    console.log(6);
  })
  .then(()=>{
    console.log(7);
  });

如果你心中已经有了答案,不妨先记下来,我们马上揭晓谜底!


✨ 核心知识点:Promise 状态吸收(State Absorption)

为什么这道题容易错?因为它引入了一个“套娃”操作:用一个 Promise (p1) 去解决另一个 Promise (p2) 。

这就是 Promise 规范中的一个核心机制——Promise Resolution Procedure,俗称状态吸收。

🔄 状态吸收:Promise 界的“移魂大法”

想象一下,p2 是一个年轻的学徒,p1 是一个已功成名就的大侠。

当我们在 p2 的构造函数中调用 resolve(p1) 时,就相当于:

学徒 p2 对大侠 p1 说:“我决定,我的命运就由您来决定了!”

根据 Promises/A+ 规范:

  1. 当一个 Promise (p2) 被另一个 Promise (p1) 解决时,p2 不会立即进入 fulfilled 状态。
  2. 相反,p2 会吸收(Adopt) p1 的状态。
  3. 这意味着,p2 上的所有 .then() 回调,都会被转移到 p1 上去执行。p2 成了一个代理(Proxy) 。

在本题中:

  • p1 = Promise.resolve(),它已经是 fulfilled 状态。
  • p2 吸收 p1 的状态,所以 p2 的回调 (1, 2, 3) 实际上是挂在了 p1 的回调队列中,和 p1 自己的回调 (4, 5, 6, 7) 一起排队。

总结: 所有的 .then() 回调,无论是来自 p1 链还是 p2 链,现在都在 同一个微任务队列 中,等待 p1 解决后执行。


🔧 调度分析:微任务队列的“插队”艺术

既然所有的回调都在一个队列里,那么它们的执行顺序就取决于两个因素:

  1. .then() 的调用顺序:决定了初始回调的入队顺序。
  2. Promise 链的连续性:决定了后续回调的入队和执行顺序。

第一步:初始入队

同步代码执行时,由于p2在准备阶段,所以p1.then(4) 先被调用。

  • 微任务队列初始状态:[p2准备阶段,p1.then(4),p2吸收阶段,p1.then(5)]

第二步:实际执行与链式调度

在 V8 引擎(Node.js/Chrome)中,Promise 链的调度有一个特性:当一个 Promise 链中的回调执行完毕后,它所返回的新 Promise 的下一个 .then() 回调,会被优先安排到当前微任务队列的末尾。

首先是最直观的过程:

状态吸收:准备、吸收

微队列:

  1. p2准备阶段
  2. p1推入4
  3. p2吸收阶段
  4. p1推入5
  5. p2推入1
  6. p1推入6
  7. p2推入2
  8. p1推入7
  9. p2推入3

现在来模拟执行过程:

序号执行任务输出解释
1p1.then(4)4注意: 尽管 p2.then(1) 先入队,但实际运行时,p1 链的第一个回调被优先执行。输出 4。p1 链的下一个回调 p1.then(5) 立即入队。
2p1.then(5)5p1 链的连续性得到体现,p1.then(5) 紧接着执行。输出 5。p1.then(6) 立即入队。
3p2.then(1)1此时,轮到 p2 链的第一个回调执行。输出 1。p2 链的下一个回调 p2.then(2) 立即入队。
4p1.then(6)6再次回到 p1 链。输出 6。p1.then(7) 立即入队。
5p2.then(2)2回到 p2 链。输出 2。p2.then(3) 立即入队。
6p1.then(7)7p1 链的最后一个回调。输出 7。
7p2.then(3)3p2 链的最后一个回调。输出 3。

运行结果图

image.png

最终的正确输出顺序是:

4, 5, 1, 6, 2, 7, 3


💡 总结:面试官想考察你什么?

通过这道题,面试官想考察你的知识点清单:

  1. 同步代码优先:console.log(p1) 和 console.log(p2) 总是最先执行。
  2. Promise 状态吸收:当 resolve(Promise) 时,被解决的 Promise (p2) 会将自己的回调转嫁给传入的 Promise (p1),导致所有回调在同一个微任务队列中竞争。
  3. 微任务调度:在 V8 引擎中,Promise 链的执行具有连续性。一旦开始执行某个 Promise 链的回调,它会倾向于执行完该链中所有已准备好的后续回调,直到遇到一个尚未解决的 Promise 或队列中没有该链的后续任务为止。

记住这个机制,下次遇到 Promise 套娃题,你就能轻松应对了!希望这篇博客对你有所帮助,我们下次见!

检测题

了解状态吸收后,来看看下面的输出结果是多少呢?

async function async1() {
  console.log(1);
  await async2();
  console.log('AAA');
}

async function async2() {
  return Promise.resolve(2);
}

async1();

Promise.resolve()
  .then(() => {
    console.log(3);
  })
  .then(() => {
    console.log(4);
  })
  .then(() => {
    console.log(5);
  });