搞清楚 Promise & async/await & 事件循环
摘要: Promise 和 async/await 的核心原理基于状态机制与任务优先级。Promise 通过不可变状态(pending/fulfilled/rejected)和链式回调管理异步,构造函数同步执行,then 回调异步执行并遵循微任务优先级。async/await 是 Promise 的语法糖,await 会暂停函数执行并注册微任务。事件循环中,执行顺序为同步代码 > 微任务(P
要彻底掌握 Promise 和 async/await 的执行原理,需从 状态机制、任务优先级、语法特性 三个维度系统梳理,结合之前的案例总结核心规律如下:
一、Promise 核心原理:状态驱动的异步容器
Promise 是 JavaScript 处理异步操作的标准方案,其核心是 "状态不可变" 和 "链式回调",关键特性如下:
1. 状态与状态变更
三种状态:
- pending(初始)、fulfilled(成功)、rejected(失败)。
状态不可逆:
- 一旦从 pending 变为 fulfilled 或 rejected,状态永久固定,后续操作无法改变(如 promise 调用
reject()后状态固定为rejected)。
状态触发条件:
- resolve(value) → 状态变为 fulfilled,关联值为 value;
- reject(reason) → 状态变为 rejected,关联值为 reason;
- 若 Promise 构造函数中抛出错误(如 throw new Error()),状态直接变为 rejected。
2. 构造函数与 then 方法的执行特性
构造函数同步执行:new Promise(executor) 中,executor 函数((resolve, reject) => {…})会 立即同步执行。then 方法异步执行:then(onFulfilled, onRejected) 是异步的,其回调函数会在 同步代码执行完毕后 执行,且:- 只有当 Promise 状态变为 fulfilled 时,onFulfilled 才会执行(参数为 resolve 的值);
- 只有当 Promise 状态变为 rejected 时,onRejected 才会执行(参数为 reject 的值);
- then 方法 返回一个新的 Promise(状态由回调函数的返回值决定),因此支持链式调用(如 b.then().then().then() 链式执行)。
3. 关键特性:值穿透与回调忽略
值穿透:若 then 的参数不是函数(如普通值、Promise 对象),则当前 then会被忽略,上一个 Promise 的值会直接传递给下一个有效的 then(如Promise.resolve(1) .then(2) .then(console.log) // then(2) 被忽略,最终打印 1回调执行时机:then 的回调会被放入 微任务队列,需等待同步代码执行完毕后才会执行Promise.resolve().then(() => console.log(2)) // console.log(2) 会在所有同步代码后执行
二、async/await:Promise 的语法糖,简化异步流程
async/await 是 ES2017 引入的语法,
本质是Promise 的封装,让异步代码写法更接近同步,核心规则如下:
1. async 函数的返回值
- 任何
async 函数(如 async function m() {})调用后必然返回一个 Promise: - 若
函数内返回普通值(如 return 1),则 Promise 状态为fulfilled,值为该普通值; - 若
函数内返回 Promise(如 return Promise.resolve(2)),则直接返回该 Promise; - 若
函数内抛出错误(如 throw new Error()),则 Promise 状态为rejected。
2. await 的核心作用:暂停与微任务注册
await 是 async/await 的核心,仅能在 async 函数中使用,
作用是 "等待异步操作完成,并暂停当前函数"。
具体流程:
第一步:处理右侧表达式(如 await async2() 或 await 1):
若表达式结果是 Promise(如 async2() 返回的 Promise),则等待其状态变更;
若表达式结果是普通值(如 1),则自动包装为 Promise.resolve(普通值)。第二步:暂停当前 async 函数的执行,将 await 后续的代码(如 console.log(‘async1 end’))打包成微任务,放入微任务队列。第三步:函数暂停期间,JavaScript 引擎继续执行外部同步代码(如 async1() 调用后,继续执行 new Promise(…))。第四步:当 await 右侧的 Promise 状态变为 fulfilled 后,从微任务队列中取出暂停的代码继续执行(如 async2() 完成后,async1 才会执行 console.log(‘async1 end’))。
三、事件循环:任务优先级决定执行顺序
Promise 和 async/await 的执行顺序,本质是 JavaScript 事件循环(Event Loop)中 任务优先级 的体现,
核心规则:同步代码 > 微任务 > 宏任务。
1. 任务类型划分
同步代码:立即执行的代码(如函数内同步打印、Promise 构造函数内的代码),优先级最高。微任务(Microtask):
- Promise 的 then/catch/finally 回调;
- async/await 中 await 后续的代码;
- Promise.resolve()/Promise.reject() 触发的回调。
微任务在 同步代码执行完毕后立即执行,且执行完所有微任务后才会处理宏任务。
宏任务(Macrotask):
- setTimeout/setInterval 回调;
- DOM 事件回调、fetch 回调等。
宏任务在 所有微任务执行完毕后 才会执行,且按加入队列的顺序执行(如 setTimeout 延迟时间不影响优先级,仅影响加入队列的时机)。
2. 执行流程口诀
微任务先于宏任务:同步代码执行完毕后,清空所有微任务(按注册顺序);
宏任务按序执行:微任务清空后,执行一个宏任务,然后重复检查微任务(若有则执行),形成循环。
四、实战分析步骤:三步解决所有执行问题
遇到 Promise/async/await 执行顺序问题时,按以下步骤分析,可确保不出错:
步骤 1:标记所有同步代码,按顺序执行并记录输出
包括:函数内的同步打印(如 console.log(‘async1 start’))、Promise 构造函数内的代码(如 new Promise((resolve) => { console.log(‘promise1’) }))、变量赋值的同步部分。
执行时忽略所有异步操作(如 then 回调、setTimeout),仅记录同步输出。
步骤 2:收集微任务,按注册顺序执行
同步代码执行完毕后,收集所有已触发的微任务(如 then 回调、await 后续代码),按它们被注册到队列的顺序执行,记录输出。
注意:await 后续代码的注册时机是在 await 右侧的 Promise 状态变为 fulfilled 之后(如 await b 需等 b 完成后,后续代码才会进入微任务队列)。
步骤 3:收集宏任务,按注册顺序执行
微任务全部执行完毕后,按注册顺序执行宏任务(如 setTimeout 回调),记录输出。
若宏任务执行过程中产生新的微任务 / 宏任务,重复步骤 2-3(先微后宏)。
五、典型陷阱总结
- Promise 构造函数同步执行:不要误以为 Promise 内的代码是异步的(如 new Promise(() => console.log(1)) 会立即打印 1)。
- await 会暂停函数,但不阻塞外部同步代码:async1() 中 await async2() 会暂停 async1,但外部代码(如 new Promise(…))会继续执行。
- then 回调的参数必须是函数,否则值穿透:then(2) 或 then(Promise.resolve(3)) 会被忽略,上一个值直接传递。
- 避免循环等待(死锁):await a 中若 a 是自身的 Promise(状态为 pending),会导致永远等待
a = new Promise(async (resolve) => { await a; resolve(); })
通过以上原理和步骤,所有 Promise/async/await 的执行问题都能拆解为 “同步代码→微任务→宏任务” 的顺序分析,核心是抓住 状态变化 和 任务优先级 两个关键点,即可做到 “一通百通”。
更多推荐



所有评论(0)