从回调地狱开始
在 Promise 出现之前,JavaScript 的异步操作只能用回调函数处理。多个异步操作串联时,会形成所谓的"回调地狱":
fetchUser(userId, (err, user) => {
if (err) return handleError(err);
fetchPosts(user.id, (err, posts) => {
if (err) return handleError(err);
fetchComments(posts[0].id, (err, comments) => {
if (err) return handleError(err);
// 终于拿到了 comments
});
});
});
这种代码有几个严重问题:
- 嵌套太深,缩进失控:代码从左边界向右边界无限延伸
- 错误处理重复:每个层级都要单独处理错误
- 代码不可复用:如果
fetchUser的结果需要被多处使用,复制粘贴是唯一选择 - 难以组合:两个异步操作的结果需要组合时,代码会变得极其复杂
Promise 的基本概念
Promise 是 JavaScript 对异步结果的状态机封装。它代表一个异步操作的最终结果,可能成功也可能失败。
一个 Promise 有三种状态:
- Pending(待定):初始状态,既不是成功也不是失败
- Fulfilled(已兑现):操作成功完成
- Rejected(已拒绝):操作失败
Promise 只能转换一次状态,一旦从 pending 变成 fulfilled 或 rejected,就不能再变化。这个"一次性"特性让 Promise 容易推理。
Promise 的基本用法
function fetchUser(id) {
return new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
if (id > 0) {
resolve({ id, name: 'User' + id });
} else {
reject(new Error('Invalid user ID'));
}
}, 100);
});
}
fetchUser(1)
.then(user => {
console.log(user); // { id: 1, name: 'User1' }
return fetchUser(2);
})
.then(user => {
console.log(user); // { id: 2, name: 'User2' }
})
.catch(err => {
console.error(err); // 处理前面任何一步的错误
});
then 方法接受两个回调函数作为参数:第一个是 onFulfilled(成功时调用),第二个是 onRejected(失败时调用)。.catch(err => ...) 等价于 .then(null, err => ...)。
.then() 返回一个新的 Promise,新 Promise 的决议值是回调函数的返回值。如果回调抛出异常,新 Promise 会被 rejected。
Promise 链式调用的原理
Promise 链式调用的关键是:.then() 总是返回一个新的 Promise。
Promise.resolve(1)
.then(x => x + 1) // 返回 Promise resolved 为 2
.then(x => x + 1) // 返回 Promise resolved 为 3
.then(console.log); // 输出 3
但如果 .then() 的回调返回一个 Promise:
fetchUser(1)
.then(user => {
return fetchPosts(user.id); // 这里返回一个 Promise
})
.then(posts => {
// 这里 posts 是 fetchPosts resolved 的值,不是 Promise 本身
console.log(posts);
});
这个行为叫Promise 展开:如果 .then() 的回调返回一个 Promise,链会等待这个 Promise resolved,然后继续传递它的 resolved 值。这就是为什么异步操作能"扁平化"串联,而不是无限嵌套。
Promise.all、Promise.race、Promise.allSettled
处理多个 Promise 时,Promise 提供了几个组合工具:
Promise.all:所有 Promise 都成功时 resolved,结果是一个 resolved 值的数组;任意一个 rejected 时,整体立即 rejected。
Promise.all([fetchUser(1), fetchUser(2), fetchUser(3)])
.then(([user1, user2, user3]) => {
console.log(user1, user2, user3);
})
.catch(err => {
// 如果任何一个 rejected,这里会被调用
});
典型场景:并行获取多个资源,等待所有资源都加载完成后再渲染。
Promise.race:任意一个 Promise settled(resolved 或 rejected),就采用它的结果。
Promise.race([
fetchWithTimeout(resource1, 1000),
fetchWithTimeout(resource2, 2000)
])
.then(result => {
console.log('第一个返回的是', result);
});
典型场景:多个数据源竞争,只取最快返回的那个,加上超时控制。
Promise.allSettled:所有 Promise 都 settled 后返回,不管成功还是失败。
Promise.allSettled([fetchUser(1), fetchUser(-1), fetchUser(3)])
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`${index}:`, result.value);
} else {
console.log(`${index}:`, result.reason);
}
});
});
典型场景:批量操作,不管成功失败都要收集所有结果。
async/await 是什么
async/await 是 Promise 的语法糖,让异步代码看起来像同步代码。
async function loadData() {
try {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return comments;
} catch (err) {
console.error('Failed to load data:', err);
}
}
async 函数总是返回一个 Promise。await 只能在 async 函数内部使用,它会暂停函数的执行,等待 Promise resolved,然后返回 resolved 的值。
await 暂停的是 async 函数内的执行,而不是整个 JavaScript 引擎。JavaScript 引擎继续执行其他代码,包括事件处理、计时器等。
async/await 和 Promise 的关系
async/await 没有引入新的异步模型,只是让 Promise 的链式调用变成了更线性的写法。
// 下面两段代码是等价的:
// Promise 链式写法
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => console.log(comments));
// async/await 写法
async function load() {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
console.log(comments);
}
async/await 能用 try/catch 来处理错误,比 Promise 的 .catch() 更符合同步代码的习惯。
但注意:await 是串行执行的。如果两个异步操作不互相依赖,应该用 Promise.all 并行执行:
// 串行:总时间 = 100ms + 100ms = 200ms
async function serial() {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
}
// 并行:总时间 = max(100ms, 100ms) = 100ms
async function parallel() {
const [user, posts] = await Promise.all([
fetchUser(1),
fetchPosts(userId)
]);
}
Promise 的常见错误
错误一:忘记 return Promise
// 错误
fetchUser(1)
.then(user => {
fetchPosts(user.id); // 漏了 return!
})
.then(posts => {
console.log(posts); // 这里 posts 是 undefined,因为上一个 then 没返回
});
// 正确
fetchUser(1)
.then(user => {
return fetchPosts(user.id);
})
.then(posts => {
console.log(posts);
});
错误二:在循环里顺序 await
// 错误:O(n) 串行时间
async function processAll(items) {
for (const item of items) {
await process(item); // 每次等待一个完成才处理下一个
}
}
// 正确:O(1) 并行时间
async function processAll(items) {
await Promise.all(items.map(item => process(item)));
}
错误三:没有正确处理 rejected Promise
// 错误:没有 catch,rejected 的 Promise 会变成未处理的 rejection
async function bad() {
const result = await someAsyncOperation(); // 如果这里 rejected,整个函数会 rejected,但没人处理
}
// 正确:总是处理可能的错误
async function good() {
try {
const result = await someAsyncOperation();
return result;
} catch (err) {
console.error(err);
return null; // 或者抛出有意义的错误
}
}
这一章想说的
Promise 是 JavaScript 对异步结果的状态机封装,代表一个异步操作的最终结果。.then() 返回新的 Promise,让链式调用成为可能。
async/await 是 Promise 的语法糖,没有引入新的异步模型,只是让代码看起来更像同步代码。但要记住 await 是串行执行的,多个不依赖的异步操作应该用 Promise.all 并行。
Promise 的常见错误包括:忘记 return、在循环里顺序 await、没处理 rejected Promise。这些错误在面试中经常出现,值得特别注意。