🌟 开场白: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+ 规范:
- 当一个 Promise (
p2) 被另一个 Promise (p1) 解决时,p2不会立即进入fulfilled状态。 - 相反,
p2会吸收(Adopt)p1的状态。 - 这意味着,
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 解决后执行。
🔧 调度分析:微任务队列的"插队"艺术
既然所有的回调都在一个队列里,那么它们的执行顺序就取决于两个因素:
.then()的调用顺序:决定了初始回调的入队顺序。- Promise 链的连续性:决定了后续回调的入队和执行顺序。
第一步:初始入队
同步代码执行时,由于p2在准备阶段,所以p1.then(4) 先被调用。
- 微任务队列初始状态:
[p2准备阶段,p1.then(4),p2吸收阶段,p1.then(5)]
第二步:实际执行与链式调度
在 V8 引擎(Node.js/Chrome)中,Promise 链的调度有一个特性:当一个 Promise 链中的回调执行完毕后,它所返回的新 Promise 的下一个 .then() 回调,会被优先安排到当前微任务队列的末尾。
首先是最直观的过程:
状态吸收:准备、吸收
微队列:
- p2准备阶段
- p1推入4
- p2吸收阶段
- p1推入5
- p2推入1
- p1推入6
- p2推入2
- p1推入7
- p2推入3
现在来模拟执行过程:
| 序号 | 执行任务 | 输出 | 解释 |
|---|---|---|---|
| 1 | p1.then(4) |
4 | 注意: 尽管 p2.then(1) 先入队,但实际运行时,p1 链的第一个回调被优先执行。输出 4 。p1 链的下一个回调 p1.then(5) 立即入队。 |
| 2 | p1.then(5) |
5 | p1 链的连续性得到体现,p1.then(5) 紧接着执行。输出 5 。p1.then(6) 立即入队。 |
| 3 | p2.then(1) |
1 | 此时,轮到 p2 链的第一个回调执行。输出 1 。p2 链的下一个回调 p2.then(2) 立即入队。 |
| 4 | p1.then(6) |
6 | 再次回到 p1 链。输出 6 。p1.then(7) 立即入队。 |
| 5 | p2.then(2) |
2 | 回到 p2 链。输出 2 。p2.then(3) 立即入队。 |
| 6 | p1.then(7) |
7 | p1 链的最后一个回调。输出 7。 |
| 7 | p2.then(3) |
3 | p2 链的最后一个回调。输出 3。 |
运行结果图

最终的正确输出顺序是:
4, 5, 1, 6, 2, 7, 3
💡 总结:面试官想考察你什么?
通过这道题,面试官想考察你的知识点清单:
- 同步代码优先 :
console.log(p1)和console.log(p2)总是最先执行。 - Promise 状态吸收 :当
resolve(Promise)时,被解决的 Promise (p2) 会将自己的回调转嫁 给传入的 Promise (p1),导致所有回调在同一个微任务队列中竞争。 - 微任务调度 :在 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);
});