Skip to content

Promise

Gemini 2.5 pro 解释 promise 执行顺序。

好的,这是一个非常核心且重要的前端知识点。要彻底理解 JavaScript Promise 的执行顺序,关键在于理解 事件循环(Event Loop)、宏任务(Macrotask)和微任务(Microtask) 这三个概念。

我们用最直观的方式来拆解它。

1. 两大核心原则

请先记住这两条黄金法则,它们能解释 99% 的 Promise 执行顺序问题:

  1. new Promise(executor) 中的执行器函数 executor 是立即执行的(同步的)
  2. .then(), .catch(), .finally() 中注册的回调函数是异步的,它们会被放入“微任务队列(Microtask Queue)”中等待执行

2. 事件循环的简化模型

想象一下 JavaScript 的执行环境有三个关键部分:

  1. 调用栈(Call Stack):一个后进先出(LIFO)的栈,用来执行同步代码。我们称之为“主线程”。
  2. 微任务队列(Microtask Queue):一个先进先出(FIFO)的队列,存放 Promise 回调等高优先级的异步任务。可以把它想象成一个 “VIP 等待区”
  3. 宏任务队列(Macrotask Queue):一个先进先出(FIFO)的队列,存放 setTimeout, setInterval, I/O 操作等普通优先级的异步任务。可以把它想象成一个 “普通等待区”

执行规则:

  1. 首先,执行调用栈中的所有同步代码,直到调用栈变空。
  2. 然后,检查微任务队列(VIP 等待区),执行里面所有的微任务,直到它也变空。
  3. 接着,取一个宏任务从宏任务队列(普通等待区)中拿出来,放入调用栈中执行。
  4. 执行完毕后,回到第 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 轮:执行同步代码

  1. console.log('脚本开始'): 这是同步代码,立即执行。

    • 输出: 脚本开始
    • 调用栈: console.log 入栈 -> 执行 -> 出栈。
  2. setTimeout(...): 这是异步API。

    • 它的回调函数 function() { console.log('setTimeout 执行'); } 被注册,并被浏览器/Node.js 引擎放入 宏任务队列
    • 主线程继续向下执行。
  3. 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 结束
  4. 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. 精炼总结

  1. 同步代码 > 微任务 > 宏任务
  2. new Promise 的执行器是同步的。
  3. Promise.then/catch/finally 的回调是微任务
  4. setTimeout/setInterval 的回调是宏任务
  5. await 关键字后面的代码可以看作是微任务
  6. 一个事件循环周期内,会清空所有微任务,但只处理一个宏任务。