JS中await、Promise与setTimeout的代码执行优先级解析
我在梳理JavaScript中包含await时的代码执行顺序时遇到了困惑,以下是两个测试示例及问题:
示例1
const a = Promise.resolve(98); const b = Promise.resolve(78); const c = Promise.resolve(45); const e = Promise.resolve(23); setTimeout(() => console.log("hi8")); const d = c.then(() => { console.log("hi9") }); console.log("hi0"); async function lome() { console.log("hi1"); await setTimeout(() => console.log("hi7")); console.log("hi2"); } lome2(); lome(); console.log("hi3"); async function lome2() { console.log("hi10"); await e.then(() => { console.log("hi11") }); console.log("hi12"); } setTimeout(() => console.log("hi4")); b.then(() => console.log("hi6"));
输出顺序:
hi0 hi10 hi1 hi3 hi9 hi11 hi7 hi6 hi12 hi2 hi8 hi4
从这个输出我最初推测:同步代码优先执行,接着是Promise回调,然后是await后的代码,最后是setTimeout回调——即await后代码优先级高于setTimeout、低于Promise回调。
示例2
const a = Promise.resolve(98); const b = Promise.resolve(78); const c = Promise.resolve(45); const e = Promise.resolve(23); setTimeout(() => console.log("hi8")); const d = c.then(() => { console.log("hi9") }); console.log("hi0"); async function lome() { console.log("hi1"); await setTimeout(() => console.log("hi7")); console.log("hi2"); } lome2(); lome(); async function lome2() { console.log("hi10"); await e.then(() => { console.log("hi11") }); console.log("hi12"); } setTimeout(() => console.log("hi4")); b.then(() => console.log("hi6")); console.log("hi3");
输出顺序:
hi0 hi10 hi1 hi3 hi9 hi11 hi2 hi6 hi12 hi8 hi7 hi4
但这个示例打破了之前的推测:同步代码执行后,Promise回调(d和lome2中的回调)执行完毕,本该执行b的Promise回调输出hi6,却先输出了lome函数中await后的hi2。
核心规则:事件循环中的微任务与宏任务
JavaScript的执行顺序本质由事件循环决定,任务分为两类,优先级从高到低:
- 同步代码:立即执行,占据主线程;
- 微任务(Microtasks):同步代码执行完后立即处理,包括:
- Promise的
then/catch/finally回调; async/await中await后的代码(本质是Promise的then回调);queueMicrotask创建的任务;
- Promise的
- 宏任务(Macrotasks):微任务队列清空后才会处理,包括:
setTimeout/setInterval回调;- DOM事件回调;
- I/O操作回调;
关键细节:await的本质
await x的执行逻辑可以拆解为:
- 先同步执行
x的代码; - 如果
x不是Promise,JavaScript会自动将其包装为Promise.resolve(x); - 将
await之后的代码包装成一个微任务,加入微任务队列; - 暂停当前
async函数,让出主线权,继续执行后续同步代码。
注意:如果x是一个已决议的Promise(比如Promise.resolve(xxx)),那么await后的微任务会立即被加入队列;如果x是一个未决议的Promise,那么要等Promise决议后,才会将后续代码加入微任务队列。
示例2的执行流程拆解
我们一步步理清楚示例2的执行顺序:
1. 同步代码阶段
- 定义所有Promise变量(均为已决议状态);
- 注册
setTimeout(hi8)宏任务; - 注册
c.then(hi9)微任务(因为c已决议,直接加入队列); - 输出
hi0; - 调用
lome2():- 输出
hi10; - 执行
await e.then(hi11):先注册e.then(hi11)微任务,await等待这个e.then返回的Promise决议,此时hi12的微任务暂未加入队列; - 暂停
lome2,回到同步代码;
- 输出
- 调用
lome():- 输出
hi1; - 同步执行
setTimeout(hi7),注册hi7宏任务; await的是Promise.resolve(定时器ID)(因为setTimeout返回数字),这个Promise立即决议,所以console.log(hi2)的微任务被加入队列;- 暂停
lome,回到同步代码;
- 输出
- 注册
setTimeout(hi4)宏任务; - 注册
b.then(hi6)微任务(b已决议,直接加入队列); - 输出
hi3;
2. 微任务队列处理阶段
同步代码执行完毕后,微任务队列的初始顺序是:c.then(hi9) → e.then(hi11) → hi2 → b.then(hi6)
- 执行
c.then(hi9),输出hi9; - 执行
e.then(hi11),输出hi11;此时e.then返回的Promise决议,hi12的微任务被加入队列; - 执行
hi2,输出hi2; - 执行
b.then(hi6),输出hi6; - 执行
hi12,输出hi12;
3. 宏任务队列处理阶段
微任务队列清空后,开始处理宏任务队列,顺序是:hi8 → hi7 → hi4,依次输出对应的内容。
这就完全匹配了示例2的输出顺序,而之前的错误推测是因为没搞清楚await后的代码是微任务,以及微任务队列的添加顺序。
关于示例1输出的说明
示例1的输出中hi7(宏任务)出现在hi6(微任务)之前,这不符合事件循环规则,大概率是输出记录错误。按照正确逻辑,hi6作为微任务,应该在所有宏任务(包括hi7)之前执行。
内容的提问来源于stack exchange,提问作者lome1446

