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

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

各位掘友,大家好!

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

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

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


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

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

javascript 复制代码
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

现在来模拟执行过程:

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

运行结果图

最终的正确输出顺序是:

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 套娃题,你就能轻松应对了!希望这篇博客对你有所帮助,我们下次见!

检测题

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

js 复制代码
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);
  });
相关推荐
EnCi Zheng9 分钟前
M5-markconv自定义CSS样式指南 [特殊字符]
前端·css·python
kyriewen13 分钟前
你的网页慢,用户不说直接走——前端性能监控教你“读心术”
前端·性能优化·监控
广州华水科技13 分钟前
北斗GNSS变形监测在大坝安全监测中的应用与优势分析
前端
前端老石人25 分钟前
前端开发中的 URL 完全指南
开发语言·前端·javascript·css·html
CAE虚拟与现实25 分钟前
五一假期闲来无事,来个前段、后端的说明吧
前端·后端·vtk·three.js·前后端
Sarvartha36 分钟前
三目运算符
linux·服务器·前端
晓晨的博客43 分钟前
ROS1录制的bag包转换为ROS2格式
前端·chrome
Wect1 小时前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·typescript
donecoding1 小时前
别再让 pnpm 跟着 nvm 跑了!独立安装终极指南
前端·node.js·前端工程化
GISer_Jing1 小时前
AI全栈转型_TS后端学习路线
前端·人工智能·后端·学习