大模型也栽跟头的 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);
  });
相关推荐
非凡ghost3 小时前
MousePlus(鼠标增强工具) 中文绿色版
前端·windows·计算机外设·软件需求
焚 城3 小时前
EXCEL(带图)转html【uni版】
前端·html·excel
我家媳妇儿萌哒哒3 小时前
Vue2 elementUI年份区间选择组件
前端·javascript·elementui
笨笨狗吞噬者3 小时前
【uniapp】小程序体积优化,分包异步化
前端·微信小程序·uni-app
该用户已不存在3 小时前
Golang 上传文件到 MinIO?别瞎折腾了,这 5 个库拿去用
前端·后端·go
snows_l3 小时前
JavaScript 性能优化实战大纲
前端
文心快码BaiduComate3 小时前
文心快码3.5S开发古风射覆小游戏,它帅到我了!
前端·后端·程序员
CptW3 小时前
Vue3 的“批量渲染”机制
vue.js·面试
佛系菜狗4 小时前
防抖和节流-防抖鸿蒙版本实现
前端