一、为什么需要 Promise?
在 JavaScript 中,异步操作(如网络请求、文件读取、定时器)通常通过回调函数处理。但多个异步操作依赖或串行时,会出现回调地狱:
javascript
// 回调地狱示例
setTimeout(() => {
console.log('第一步');
setTimeout(() => {
console.log('第二步');
setTimeout(() => {
console.log('第三步');
}, 1000);
}, 1000);
}, 1000);
回调地狱的缺点:
- 代码横向增长,可读性差
- 难以捕获错误
- 难以并行或竞速处理多个异步任务
Promise 正是为了解决这些问题而诞生的,它让异步代码更接近同步写法的风格,且提供了统一的错误处理机制。
二、Promise 是什么?
Promise 是 ES6 新增的一个内置构造函数,代表一个异步操作的最终完成(或失败)及其结果值。
字面理解:Promise 是一个"承诺",承诺在未来某个时刻会给你一个结果(成功或失败)。
一个 Promise 对象处于以下三种状态之一:
| 状态 | 含义 | 特点 |
|---|---|---|
pending |
初始状态,未完成也未拒绝 | 可以转变为 fulfilled 或 rejected |
fulfilled |
操作成功完成 | 有一个 value(结果值) |
rejected |
操作失败 | 有一个 reason(失败原因) |
状态一旦改变,就永久固定,不会再变。
三、Promise 的基本用法
1. 创建一个 Promise
使用 new Promise(executor),其中 executor 是一个立即执行的函数,它接收两个参数:resolve 和 reject。
javascript
const promise = new Promise((resolve, reject) => {
// 异步操作
const success = true;
setTimeout(() => {
if (success) {
resolve('成功的数据'); // 将状态改为 fulfilled
} else {
reject('失败的原因'); // 将状态改为 rejected
}
}, 1000);
});
2. 使用 .then() 和 .catch() 消费 Promise
javascript
promise
.then((value) => {
console.log('成功:', value);
})
.catch((error) => {
console.error('失败:', error);
});
.then()接收两个可选参数:onFulfilled和onRejected。通常只传第一个,错误交给.catch()。.catch()是.then(null, onRejected)的语法糖。
3. .finally() ------ 无论成功失败都会执行
javascript
promise
.then(value => console.log(value))
.catch(error => console.error(error))
.finally(() => console.log('操作结束,清理资源'));
四、Promise 的链式调用(核心特性)
.then() 或 .catch() 返回一个新的 Promise,因此可以串联多个异步操作。
javascript
new Promise((resolve) => {
resolve(1);
})
.then((value) => {
console.log(value); // 1
return value + 1;
})
.then((value) => {
console.log(value); // 2
return value + 1;
})
.then((value) => {
console.log(value); // 3
});
链式调用中的返回值处理规则:
- 如果回调函数返回一个普通值 (包括
undefined),它会自动被包装成一个fulfilled的 Promise。 - 如果返回一个 Promise ,那么下一个
.then()会等待这个 Promise 的结果。 - 如果抛出异常 (
throw new Error()),会进入下一个.catch()。
示例:模拟异步任务顺序执行
javascript
function asyncTask(name, duration) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`${name} 完成`);
resolve(name);
}, duration);
});
}
asyncTask('任务1', 1000)
.then(() => asyncTask('任务2', 500))
.then(() => asyncTask('任务3', 800))
.then(() => console.log('所有任务完成'));
五、Promise 的静态方法
1. Promise.resolve(value)
快速返回一个状态为 fulfilled 的 Promise。
javascript
Promise.resolve('直接成功').then(console.log); // 直接成功
如果 value 本身是一个 Promise,则直接返回该 Promise。
2. Promise.reject(reason)
快速返回一个状态为 rejected 的 Promise。
javascript
Promise.reject('出错了').catch(console.error); // 出错了
3. Promise.all(iterable)
并行执行多个 Promise,所有都成功 才成功,结果按顺序组成数组;任何一个失败则立即失败,并返回第一个失败的原因。
javascript
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = new Promise((resolve) => setTimeout(resolve, 1000, 3));
Promise.all([p1, p2, p3]).then((results) => {
console.log(results); // [1, 2, 3] (等待 1 秒后输出)
});
失败示例:
javascript
const p1 = Promise.resolve(1);
const p2 = Promise.reject('错误');
Promise.all([p1, p2]).catch((err) => {
console.log(err); // '错误'
});
4. Promise.race(iterable)
竞速:返回最先 settled(成功或失败)的那个 Promise 的结果。
javascript
const p1 = new Promise((resolve) => setTimeout(resolve, 500, '慢'));
const p2 = new Promise((resolve) => setTimeout(resolve, 100, '快'));
Promise.race([p1, p2]).then(console.log); // '快'
常用于设置超时:
javascript
function fetchWithTimeout(url, timeout = 5000) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
);
return Promise.race([fetchPromise, timeoutPromise]);
}
5. Promise.allSettled(iterable) (ES2020)
等待所有 Promise 都 settle(成功或失败),返回每个对象的状态和结果(或原因)。永远不会进入 .catch()。
javascript
const promises = [
Promise.resolve('成功'),
Promise.reject('失败'),
Promise.resolve('另一个成功'),
];
Promise.allSettled(promises).then((results) => {
console.log(results);
/*
[
{ status: 'fulfilled', value: '成功' },
{ status: 'rejected', reason: '失败' },
{ status: 'fulfilled', value: '另一个成功' }
]
*/
});
6. Promise.any(iterable) (ES2021)
只要有一个 Promise 成功就返回那个成功的结果;如果所有都失败 ,则返回一个 AggregateError(聚合错误)。
javascript
const p1 = Promise.reject('错误1');
const p2 = Promise.reject('错误2');
const p3 = Promise.resolve('成功');
Promise.any([p1, p2, p3]).then(console.log); // '成功'
全部失败的情况:
javascript
Promise.any([Promise.reject('a'), Promise.reject('b')])
.catch((err) => {
console.log(err instanceof AggregateError); // true
console.log(err.errors); // ['a', 'b']
});
六、错误处理的最佳实践
-
总是添加
.catch()或者在每个.then()的第二个参数中处理错误,否则错误会被静默吞没(虽然浏览器会报 unhandled rejection,但不推荐依赖于此)。 -
在链式调用的末尾统一捕获错误:
javascript
doSomething()
.then(doSomethingElse)
.then(doThirdThing)
.catch(handleError); // 捕获前面任意一步的错误
-
不要在
.then()中抛出普通字符串 ,而应该抛出Error对象,以便获得堆栈信息。 -
async/await与 try-catch 是 Promise 的语法糖,但底层依然是 Promise,理解 Promise 是掌握async/await的基础。
七、Promise 与回调地狱对比
回调地狱:
javascript
getUser((user) => {
getPosts(user.id, (posts) => {
getComments(posts[0].id, (comments) => {
console.log(comments);
});
});
});
Promise 版本(假设每个函数都返回 Promise):
javascript
getUser()
.then(user => getPosts(user.id))
.then(posts => getComments(posts[0].id))
.then(comments => console.log(comments))
.catch(error => console.error(error));
扁平化,更易读。
八、进阶示例:串行与并行控制
1. 串行执行一组异步任务(逐个进行)
javascript
const tasks = [task1, task2, task3]; // 每个 task 返回 Promise
tasks.reduce((prevPromise, task) => {
return prevPromise.then(() => task());
}, Promise.resolve());
2. 限制并发数量(例如同时最多 3 个请求)
javascript
async function limitConcurrent(tasks, limit) {
const results = [];
const executing = [];
for (const task of tasks) {
const p = task().then(r => results.push(r));
executing.push(p);
if (executing.length >= limit) {
await Promise.race(executing);
// 清除已完成的任务
executing.splice(executing.findIndex(e => e === p), 1);
}
}
await Promise.all(executing);
return results;
}
九、Promise 的局限性
| 问题 | 说明 |
|---|---|
| 不可取消 | 一旦创建 Promise,无法从外部取消它的执行(某些库提供 cancelable promise,但非标准)。 |
| 无法获取中间状态 | 无法同步获取"进度",只能拿到最终结果(不过可以自己实现进度通知)。 |
| 错误可能被吞没 | 若没有 .catch() 且未被 async/await 捕获,错误将静默消失(现代浏览器会打印 unhandledrejection 警告)。 |
| 新手容易误解 | 例如忘记 return 导致链断裂,或在 .then() 中创建新的 Promise 但未返回。 |
十、总结
- Promise 是异步编程的基石,它将回调函数的嵌套转化为链式调用。
- 核心掌握:状态 (pending/fulfilled/rejected)、链式调用 、静态方法 (
all、race、allSettled、any)。 - 搭配
async/await使用时,Promise 依然在幕后工作,理解 Promise 有助于写出更可靠的异步代码。 - 实际开发中,几乎所有现代 JavaScript API(如
fetch、fs.promises、setTimeout包装)都基于 Promise。