在现代前端开发中,我们经常需要处理多个异步操作,比如同时从多个 API 获取数据。以获取 GitHub 用户仓库信息为例:
js
const urls = [
'https://api.github.com/users/shunwuyu/repos',
'https://api.github.com/users/LeetAt67/repos'
];
当我们需要同时获取 shunwuyu
和 LeetAt67
这两个用户的仓库列表时,就面临一个典型的并发异步任务场景。这时,Promise.all
就成为了我们的最佳选择。
什么是 Promise.all
Promise.all
是 JavaScript 中 Promise
构造函数的一个静态方法(static method),用于处理多个 Promise 的并发执行。它接收一个可迭代对象(通常是数组),其中每个元素都是一个 Promise。
js
// 基本语法
Promise.all(iterableOfPromises)
.then(results => {
// 所有 Promise 成功解决时执行
})
.catch(error => {
// 任何一个 Promise 被拒绝时执行
});
核心特性解析
1. 全部成功才成功
Promise.all
最重要的特性是原子性 :只有当所有传入的 Promise 都成功解决(fulfilled)时,Promise.all
返回的 Promise 才会解决。只要有一个 Promise 被拒绝(rejected),整个 Promise.all
就会立即被拒绝。
js
// 示例:模拟 GitHub API 请求
const fetchRepos = (username) => {
return fetch(`https://api.github.com/users/${username}/repos`)
.then(response => {
if (!response.ok) {
throw new Error(`Failed to fetch repos for ${username}`);
}
return response.json();
});
};
const promises = [
fetchRepos('shunwuyu'),
fetchRepos('LeetAt67')
];
Promise.all(promises)
.then(results => {
// 只有当两个请求都成功时才会执行
console.log('User 1 repos:', results[0]);
console.log('User 2 repos:', results[1]);
})
.catch(error => {
// 任何一个请求失败都会触发 catch
console.error('Error fetching repositories:', error.message);
});
2. 结果的有序性
Promise.all
保证结果的顺序与输入 Promise 的顺序完全一致,即使它们的完成时间不同。
js
const slowPromise = new Promise(resolve =>
setTimeout(() => resolve('Slow'), 1000)
);
const fastPromise = new Promise(resolve =>
setTimeout(() => resolve('Fast'), 100)
);
Promise.all([slowPromise, fastPromise])
.then(results => {
console.log(results); // ['Slow', 'Fast']
// 注意:尽管 fastPromise 先完成,但它在结果数组中的位置仍然是第二个
});
3. 静态方法的本质
Promise.all
是 Promise
构造函数的静态方法,这意味着它属于 Promise
类本身,而不是某个 Promise 实例。这种设计模式在 JavaScript 中很常见,用于提供工具性功能。
js
// 正确用法
Promise.all([...]);
// 错误用法
const promise = new Promise(() => {});
promise.all([...]); // TypeError: promise.all is not a function
实际应用场景
批量数据获取
js
async function fetchMultipleUsersRepos(usernames) {
try {
const promises = usernames.map(username =>
fetch(`https://api.github.com/users/${username}/repos`)
.then(res => res.json())
);
const allRepos = await Promise.all(promises);
return allRepos;
} catch (error) {
console.error('Failed to fetch one or more repositories:', error);
throw error;
}
}
// 使用
fetchMultipleUsersRepos(['shunwuyu', 'LeetAt67'])
.then(reposArrays => {
console.log('All repositories fetched successfully');
// reposArrays[0] 是 shunwuyu 的仓库
// reposArrays[1] 是 LeetAt67 的仓库
});
资源预加载
js
function preloadAssets() {
const imagePromises = imageUrls.map(url => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
});
});
const fontPromise = document.fonts.load('1em "CustomFont"');
return Promise.all([...imagePromises, fontPromise]);
}
错误处理策略
由于 Promise.all
的"短路"特性(一个失败全部失败),我们需要谨慎处理错误:
js
// 方案1:在每个 Promise 内部处理错误
const safePromises = [
fetchRepos('shunwuyu').catch(err => ({ error: err, data: null })),
fetchRepos('LeetAt67').catch(err => ({ error: err, data: null }))
];
Promise.all(safePromises)
.then(results => {
results.forEach((result, index) => {
if (result.error) {
console.warn(`Failed to fetch user ${index + 1}:`, result.error);
} else {
console.log(`User ${index + 1} repos:`, result.data);
}
});
});
// 方案2:使用 Promise.allSettled(ES2020)
// Promise.allSettled 等待所有 Promise 完成,无论成功或失败
Promise.allSettled([
fetchRepos('shunwuyu'),
fetchRepos('LeetAt67')
]).then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`User ${index + 1} success:`, result.value);
} else {
console.error(`User ${index + 1} failed:`, result.reason);
}
});
});
性能考量
Promise.all
会并发执行所有 Promise,这通常比串行执行要快得多:
js
// 串行执行 - 慢
async function serialExecution() {
const result1 = await fetchRepos('shunwuyu');
const result2 = await fetchRepos('LeetAt67');
return [result1, result2];
}
// 并发执行 - 快
async function parallelExecution() {
const [result1, result2] = await Promise.all([
fetchRepos('shunwuyu'),
fetchRepos('LeetAt67')
]);
return [result1, result2];
}
总结
Promise.all
是处理并发异步任务的强大工具,特别适用于:
- 需要同时获取多个独立数据源的场景
- 所有数据都准备好后才能进行下一步操作的业务逻辑
- 资源预加载和初始化
理解其"全成功才成功"的特性以及结果的有序性,能够帮助我们更好地设计异步代码。同时,也要注意其错误处理的特殊性,必要时可以考虑使用 Promise.allSettled
作为替代方案。
在实际开发中,合理运用 Promise.all
可以显著提升应用性能和用户体验,让我们的异步代码更加简洁高效。