彻底吃透 Promise:从状态、链式到手写实现,再到 async/await 底层原理
面试必考,源码必问,日常必用 —— Promise 是 JavaScript 异步编程的基石。本文带你完整梳理 Promise 的核心知识,并深入
async/await的底层实现。
一、为什么需要 Promise?
在 Promise 出现之前,我们靠回调函数处理异步。回调模式有三个致命问题:
- 回调地狱:异步任务层层嵌套,代码横向发展(金字塔结构),难以阅读和维护。
- 错误处理混乱:每个回调必须单独处理错误,容易遗漏;
try/catch无法捕获异步回调中的异常。 - 并发组合困难:并行执行多个任务并在全部完成后执行逻辑,需要手动计数器,极易出错。
- 信任问题(控制反转):将回调交给第三方库后,无法保证它会被正确调用(次数、时机、参数等)。
Promise 应运而生,它通过状态机 + 链式调用 + 统一错误处理 + 组合工具,彻底改变了异步编程的体验。
二、Promise 核心概念速览
2.1 三种状态
pending(进行中):初始状态。fulfilled(已成功):调用resolve后到达此状态,并拥有一个最终value。rejected(已失败):调用reject后到达此状态,并拥有一个最终reason。
重要规则:状态一旦定型(settled)就不可再变,且只能从 pending 转换为 fulfilled 或 rejected。
2.2 链式调用
then、catch、finally 都返回一个新 Promise,从而实现链式。
-
then(onFulfilled, onRejected):接收成功/失败回调。返回值决定新 Promise 的状态:- 返回普通值 → 新 Promise 用该值
resolve。 - 返回 Promise → 新 Promise 的状态与该 Promise 一致。
- 抛出异常 → 新 Promise 用该错误
reject。 - 如果
onFulfilled或onRejected不是函数,会发生值穿透(原值直接传递)。
- 返回普通值 → 新 Promise 用该值
-
catch(onRejected):语法糖then(undefined, onRejected)。 -
finally(onFinally):无论成功失败都会执行,不接收参数,返回值被忽略(除非回调内抛出异常或返回 rejected Promise,则会中断链并传递新错误)。适合做清理工作。
2.3 静态方法一览
| 方法 | 行为 | 典型场景 |
|---|---|---|
Promise.all | 全部成功才成功,任一失败则立即失败 | 多个接口数据都成功后才渲染页面 |
Promise.allSettled | 等待所有定型,永不失败;返回结果状态数组 | 记录所有任务结果,即使部分失败 |
Promise.race | 最快定型的 Promise 胜出(成功或失败) | 设置超时计时 |
Promise.any | 最快成功的 Promise 胜出;全部失败才失败 | 多个备用接口,取最快成功的响应 |
Promise.resolve | 包装值为 resolved Promise | 将 thenable 转换为真正 Promise |
Promise.reject | 包装值为 rejected Promise | 快速返回失败 |
三、面试高频考点:事件循环与微任务
理解微任务(microtask)是写出正确 Promise 代码的前提。
- 宏任务:
setTimeout、setInterval、I/O、UI 渲染。 - 微任务:
Promise.then/catch/finally、queueMicrotask、MutationObserver。
执行顺序:当前宏任务 → 所有微任务 → 下一个宏任务。
经典例题:
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);
// 输出:1,4,3,2
解释:先执行同步代码(1,4),然后清空微任务队列(3),最后执行下一个宏任务(2)。
四、手写一个符合 Promise/A+ 规范的简化版 Promise
面试中常要求手写简易 Promise,核心包含:构造函数、then、catch、resolve、reject,支持异步与链式调用。
下面是一个符合规范的实现(重点注释):
class MyPromise {
constructor(executor) {
this.state = 'pending'; // 'fulfilled' | 'rejected'
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state !== 'pending') return;
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn());
};
const reject = (reason) => {
if (this.state !== 'pending') return;
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
// 值穿透处理
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };
const promise2 = new MyPromise((resolve, reject) => {
const fulfilledMicrotask = () => {
queueMicrotask(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
});
};
const rejectedMicrotask = () => {
queueMicrotask(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
});
};
if (this.state === 'fulfilled') {
fulfilledMicrotask();
} else if (this.state === 'rejected') {
rejectedMicrotask();
} else if (this.state === 'pending') {
this.onFulfilledCallbacks.push(fulfilledMicrotask);
this.onRejectedCallbacks.push(rejectedMicrotask);
}
});
return promise2;
}
catch(onRejected) {
return this.then(null, onRejected);
}
static resolve(value) {
if (value instanceof MyPromise) return value;
return new MyPromise(resolve => resolve(value));
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
}
// 辅助函数:处理 then 返回的 x(可能是普通值、Promise 或 thenable)
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected'));
}
if (x && (typeof x === 'object' || typeof x === 'function')) {
let called = false; // 防止多次调用 resolve/reject
try {
const then = x.then;
if (typeof then === 'function') {
then.call(
x,
y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
},
r => {
if (called) return;
called = true;
reject(r);
}
);
} else {
resolve(x);
}
} catch (err) {
if (called) return;
called = true;
reject(err);
}
} else {
resolve(x);
}
}
关键点说明:
- 使用
queueMicrotask模拟原生 Promise 的微任务行为。 - 支持异步:当状态为
pending时将回调存入队列,等待resolve/reject后执行。 - 支持链式:
then返回新 Promise,并通过resolvePromise解包返回值。 - 实现值穿透、错误冒泡、循环引用检测。
五、深入理解 async/await 的底层原理
async/await 是 ES2017 引入的语法糖,其底层基于 Promise + 生成器(Generator)。
5.1 生成器 + Promise 模拟 async/await
生成器函数可以暂停(yield)和恢复(next),并且可以向外部传递值。利用这一点,我们可以编写一个执行器来自动驱动生成器,每次遇到 yield 就等待 Promise 完成,然后将结果传回生成器继续执行。
以下是一个简化版的执行器 run:
function run(generatorFn) {
const gen = generatorFn();
function handle(result) {
if (result.done) return Promise.resolve(result.value);
return Promise.resolve(result.value).then(
value => handle(gen.next(value)),
error => handle(gen.throw(error))
);
}
try {
return handle(gen.next());
} catch (err) {
return Promise.reject(err);
}
}
// 使用示例
function fetchData(url) {
return new Promise(resolve => setTimeout(() => resolve(`数据来自 ${url}`), 1000));
}
const genAsync = function* () {
const data1 = yield fetchData('https://api.example.com/user');
console.log(data1);
const data2 = yield fetchData('https://api.example.com/orders');
console.log(data2);
return '完成';
};
run(genAsync).then(console.log);
这段代码的行为与 async/await 完全一致:
async function asyncFunc() {
const data1 = await fetchData('https://api.example.com/user');
console.log(data1);
const data2 = await fetchData('https://api.example.com/orders');
console.log(data2);
return '完成';
}
asyncFunc().then(console.log);
5.2 编译转换(Babel 视角)
当使用 Babel 将 async/await 编译到 ES5 时,会将其转换为生成器 + 执行器(或 Promise 链)。例如:
// 源代码
async function foo() {
const a = await bar();
return a;
}
// Babel 简化输出(类似)
function foo() {
return _asyncToGenerator(function* () {
const a = yield bar();
return a;
})();
}
其中 _asyncToGenerator 就是一个类似于上面 run 的执行器。
5.3 总结:async/await 的本质
| 层级 | 实现机制 |
|---|---|
| 最上层 | async/await 语法(开发者编写) |
| 转译/编译层 | 转换为生成器 + 执行器 或 Promise 链 |
| 执行层 | 生成器的 yield 暂停能力 + Promise 的异步通知 |
| 底层运行时 | 微任务(Microtask) + 事件循环 |
因此,理解 async/await 的关键在于掌握:
- Promise 提供了异步结果的标准表示和组合能力。
- 生成器 提供了函数执行的可暂停、可恢复能力。
- 执行器 将两者粘合,自动处理 Promise 的完成和拒绝,驱动生成器继续执行。
这也解释了为什么 async 函数总是返回 Promise,以及 await 只能出现在 async 函数中——因为生成器模式需要外部执行器驱动,而 async 函数正是这个执行器的容器。
六、高频面试题精选(附解答要点)
1. Promise 有哪几种状态?状态之间如何转换?
- 三种:
pending、fulfilled、rejected。 - 转换:
pending → fulfilled(调用resolve),pending → rejected(调用reject)。状态一旦定型不可逆。
2. then 方法返回的是什么?如何实现链式调用?
- 返回一个新 Promise。新 Promise 的状态由回调的返回值决定。通过返回新 Promise 实现链式。
3. 什么是 Promise 的“值穿透”?举例。
- 如果
then传入非函数,则忽略该参数,原值直接传递下去。
Promise.resolve(42).then(null).then(v => console.log(v)); // 42
4. finally 能改变返回值吗?
- 不能。返回值被忽略,原 Promise 的值或原因会继续传递。除非
finally回调抛出异常或返回 rejected Promise,则会传递新错误。
5. Promise.all 和 Promise.allSettled 的区别?
all:全部成功才成功,任一失败则立即失败(短路)。allSettled:等待所有定型,总是成功,返回每个结果的状态对象数组。
6. 如何捕获 Promise 链中的错误?
- 使用链尾的
.catch(),它会捕获链中任何地方抛出的错误(包括then回调中抛出的错误)。
7. 简述 Promise 的实现原理(手写简化版)。
- 状态机 + 回调队列 +
then返回新 Promise + 微任务调度。详见上文实现。
8. 什么是微任务?为什么 Promise 的回调是微任务?
- 微任务在当前宏任务执行完毕后、下一个宏任务之前执行。Promise 回调设为微任务是为了让异步结果尽快被处理,同时保持顺序可预测。
9. async/await 的底层实现是什么?
- 基于 Promise 和生成器(Generator)的语法糖。通过执行器自动驱动生成器,每次
yield一个 Promise,等待完成后恢复执行。
10. 如何将 Node.js 回调风格 API 转换为 Promise?
- 使用
util.promisify或手动new Promise包装。
七、实战:使用 Promise.race 实现请求超时
function fetchWithTimeout(url, timeoutMs = 5000) {
const fetchPromise = fetch(url).then(res => res.json());
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeoutMs)
);
return Promise.race([fetchPromise, timeoutPromise]);
}
// 使用
fetchWithTimeout('https://api.example.com/data', 3000)
.then(data => console.log(data))
.catch(err => console.error(err.message));
注意:Promise.race 不会取消未完成的请求,但可以控制超时后的行为。如果需要真正取消请求,可结合 AbortController。
八、总结
Promise 的出现统一了 JavaScript 的异步模式,解决了回调地狱、错误处理和组合难的问题。掌握 Promise 是理解现代前端异步编程的基石,而 async/await 则是在 Promise 之上的优雅语法糖,其底层依赖生成器与执行器。希望本文能帮助你彻底吃透 Promise,并在面试和实战中游刃有余。
如果觉得有帮助,欢迎点赞、收藏、评论交流!