Promise
Gemini 2.5 pro 解释 promise 执行顺序。
好的,这是一个非常核心且重要的前端知识点。要彻底理解 JavaScript Promise 的执行顺序,关键在于理解 事件循环(Event Loop)、宏任务(Macrotask)和微任务(Microtask) 这三个概念。
我们用最直观的方式来拆解它。
1. 两大核心原则⚓
请先记住这两条黄金法则,它们能解释 99% 的 Promise 执行顺序问题:
new Promise(executor)
中的执行器函数executor
是立即执行的(同步的)。.then()
,.catch()
,.finally()
中注册的回调函数是异步的,它们会被放入“微任务队列(Microtask Queue)”中等待执行。
2. 事件循环的简化模型⚓
想象一下 JavaScript 的执行环境有三个关键部分:
- 调用栈(Call Stack):一个后进先出(LIFO)的栈,用来执行同步代码。我们称之为“主线程”。
- 微任务队列(Microtask Queue):一个先进先出(FIFO)的队列,存放 Promise 回调等高优先级的异步任务。可以把它想象成一个 “VIP 等待区”。
- 宏任务队列(Macrotask Queue):一个先进先出(FIFO)的队列,存放
setTimeout
,setInterval
, I/O 操作等普通优先级的异步任务。可以把它想象成一个 “普通等待区”。
执行规则:
- 首先,执行调用栈中的所有同步代码,直到调用栈变空。
- 然后,检查微任务队列(VIP 等待区),执行里面所有的微任务,直到它也变空。
- 接着,取一个宏任务从宏任务队列(普通等待区)中拿出来,放入调用栈中执行。
- 执行完毕后,回到第 2 步,再次清空所有微任务,然后取下一个宏任务... 如此循环往复。
关键点:每次执行完一个宏任务后,都会清空所有微任务。微任务的优先级远高于宏任务。
3. 实战演练:代码示例与分步解析⚓
让我们用一个经典的例子来走一遍完整的流程:
console.log('脚本开始');
setTimeout(function() {
console.log('setTimeout 执行');
}, 0);
new Promise(function(resolve) {
console.log('Promise 开始');
resolve();
console.log('Promise 结束');
}).then(function() {
console.log('Promise.then 执行');
});
console.log('脚本结束');
3.1 分步解析:⚓
第 1 轮:执行同步代码
-
console.log('脚本开始')
: 这是同步代码,立即执行。- 输出:
脚本开始
- 调用栈:
console.log
入栈 -> 执行 -> 出栈。
- 输出:
-
setTimeout(...)
: 这是异步API。- 它的回调函数
function() { console.log('setTimeout 执行'); }
被注册,并被浏览器/Node.js 引擎放入 宏任务队列。 - 主线程继续向下执行。
- 它的回调函数
-
new Promise(...)
: 遇到new Promise
。- 根据原则 1,它的执行器函数
function(resolve) { ... }
是同步执行的。 console.log('Promise 开始')
: 同步代码,立即执行。- 输出:
Promise 开始
- 输出:
resolve()
: 这行代码将 Promise 的状态从pending
变为fulfilled
。重要的是,它会把后面.then()
里的回调函数function() { console.log('Promise.then 执行'); }
放入 微任务队列。console.log('Promise 结束')
: 同步代码,继续立即执行。- 输出:
Promise 结束
- 输出:
- 根据原则 1,它的执行器函数
-
console.log('脚本结束')
: 同步代码,立即执行。- 输出:
脚本结束
- 输出:
至此,所有同步代码执行完毕,调用栈空了。
当前状态:
* 调用栈: 空
* 微任务队列: [ then的回调函数 ]
* 宏任务队列: [ setTimeout的回调函数 ]
* 当前总输出:
脚本开始
Promise 开始
Promise 结束
脚本结束
第 2 轮:清空微任务
- 事件循环检查到调用栈已空,于是去检查微任务队列(VIP 等待区)。
- 发现里面有一个任务(
then
的回调)。 - 将该任务取出,放入调用栈执行:
function() { console.log('Promise.then 执行'); }
。 console.log('Promise.then 执行')
: 执行。- 输出:
Promise.then 执行
- 输出:
微任务队列现在空了。
当前状态:
* 调用栈: 空
* 微任务队列: 空
* 宏任务队列: [ setTimeout的回调函数 ]
* 当前总输出:
脚本开始
Promise 开始
Promise 结束
脚本结束
Promise.then 执行
第 3 轮:执行一个宏任务
- 事件循环再次检查微任务队列,发现是空的。
- 于是去宏任务队列(普通等待区)取出一个任务。
- 将
setTimeout
的回调函数放入调用栈执行:function() { console.log('setTimeout 执行'); }
。 console.log('setTimeout 执行')
: 执行。- 输出:
setTimeout 执行
- 输出:
宏任务队列现在空了。
最终结果
所有队列都已清空,程序结束。最终控制台的输出顺序为:
脚本开始
Promise 开始
Promise 结束
脚本结束
Promise.then 执行
setTimeout 执行
4. async/await
的情况⚓
async/await
是 Promise 的语法糖,它遵循同样的规则。
async
函数会隐式返回一个 Promise。await
关键字会暂停async
函数的执行。你可以把await
右边的表达式看作是同步执行的,而await
之后的所有代码,都可以理解为被放进了一个.then()
回调中,因此它们是微任务。
示例:
async function async1() {
console.log('async1 start');
await async2(); // await 后面的代码会变成微任务
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
async1();
console.log('script end');
解析:
1. script start
(同步)
2. 执行 async1()
:
* async1 start
(同步)
* 遇到 await async2()
,会立即执行 async2()
函数。
* async2
(同步)
* await
会阻塞 async1
函数内部 await
后面的代码,并将 console.log('async1 end')
注册为一个微任务。
* async1
函数的执行权交还给主线程。
3. script end
(同步)
4. 同步代码执行完毕,开始清空微任务队列。
5. async1 end
(微任务)
最终输出:
script start
async1 start
async2
script end
async1 end
5. 精炼总结⚓
- 同步代码 > 微任务 > 宏任务。
new Promise
的执行器是同步的。Promise.then/catch/finally
的回调是微任务。setTimeout/setInterval
的回调是宏任务。await
关键字后面的代码可以看作是微任务。- 一个事件循环周期内,会清空所有微任务,但只处理一个宏任务。