Promise 入门:理解 async/await 与 Promise.all
在 JavaScript 里,只要写到网络请求、定时器、文件读取这类异步任务,就绕不开 Promise。很多同学刚开始学的时候,会把 Promise、async/await、Promise.all 混在一起理解:好像用了 await 就是同步执行,好像用了 Promise.all 就一定是"并行执行"。
这篇文章基于几个简单示例,重新整理一下这些概念:Promise 的状态是什么、await 到底在等什么、什么时候应该串行等待,什么时候适合使用 Promise.all。
Promise 是什么
Promise 可以理解为一个"未来才会有结果"的对象。它刚创建出来时,结果还没有确定;过一段时间之后,要么成功,要么失败。
Promise 有三种状态:
pending:等待中,结果还没确定。fulfilled:已成功,通常由resolve(value)触发。rejected:已失败,通常由reject(reason)触发。
状态变化只有两种可能:
txt
pending -> fulfilled
pending -> rejected
一旦 Promise 从 pending 变成了 fulfilled 或 rejected,状态就固定了,之后不能再变。
需要注意一个小细节:状态名是 fulfilled,不是 fullfilled。这个拼写很容易写错。
创建一个 Promise
下面用定时器模拟一个异步天气查询:
js
function getWeather() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
temp: 38,
conditions: 'Sunny with Clouds'
});
}, 2000);
});
}
这里的 getWeather() 调用后会返回一个 Promise。2 秒后,resolve 会把 Promise 状态改为 fulfilled,并把天气数据作为成功结果传出去。
这个成功结果既可以被 .then() 的回调函数接收,也可以被 await 等到后赋值给左侧变量。
再写一个模拟获取动态列表的函数:
js
function getTweets() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(['I like cake', 'BBQ is good too!']);
}, 500);
});
}
这两个函数都遵循同一个模式:
- 调用函数时才创建 Promise。
- 创建 Promise 后开始异步任务。
- 异步任务完成后调用
resolve返回结果。
这种封装方式比直接把 Promise 写成全局变量更灵活,因为你可以控制任务什么时候开始。
await 等待的是 Promise 的结果
async/await 是 ES2017 引入的异步写法,它的价值在于:让异步代码写起来更像同步代码。
例如:
js
async function main() {
const weatherData = await getWeather();
console.log(weatherData);
}
main();
await getWeather() 的意思是:调用 getWeather() 得到一个 Promise,然后等待这个 Promise 变成 fulfilled,最后把 resolve 出来的值赋给 weatherData。
所以这句代码执行完成后,weatherData 的值就是:
js
{
temp: 38,
conditions: 'Sunny with Clouds'
}
如果 Promise 变成了 rejected,await 会抛出异常,可以用 try...catch 捕获。
js
async function main() {
try {
const weatherData = await getWeather();
console.log(weatherData);
} catch (error) {
console.error('获取天气失败:', error);
}
}
串行 await:任务会一个接一个执行
如果我们这样写:
js
async function main() {
console.time('my-operation');
const weatherData = await getWeather();
const tweetsData = await getTweets();
console.log(weatherData);
console.log(tweetsData);
console.timeEnd('my-operation');
}
main();
执行顺序是:
- 调用
getWeather(),开始等待 2 秒。 getWeather()完成后,才调用getTweets()。getTweets()再等待 0.5 秒。- 最后打印结果。
所以总耗时大约是:
txt
2000ms + 500ms = 2500ms
这种写法不是错。它适合后一项任务依赖前一项任务结果的场景。
例如:
js
const user = await getUser();
const orders = await getOrders(user.id);
这里必须先拿到 user.id,才能请求订单,所以串行等待是合理的。
但如果两个任务互不依赖,串行等待就会浪费时间。
Promise.all:同时等待多个 Promise
Promise.all 是 Promise 构造函数上的静态方法。当多个异步任务之间没有依赖关系时,可以使用它统一等待多个 Promise:
js
async function main() {
console.time('my-operation');
const [tweetsData, weatherData] = await Promise.all([
getTweets(),
getWeather()
]);
console.log(tweetsData);
console.log(weatherData);
console.timeEnd('my-operation');
}
main();
这里 getTweets() 和 getWeather() 会在创建 Promise 时就启动各自的异步任务。Promise.all 会等待两个 Promise 都成功后,再把结果组成数组返回。
因为 getTweets() 大约 0.5 秒完成,getWeather() 大约 2 秒完成,所以总耗时大约是:
txt
max(500ms, 2000ms) = 2000ms
这就是它相比串行等待更快的原因。
不过要把一句话说准确:Promise.all 本身不是"开启并行"的魔法开关。真正启动任务的是 getTweets() 和 getWeather() 的调用。Promise.all 的作用是统一等待这些 Promise,并在全部成功后收集结果。
Promise.all 的结果顺序
Promise.all 有一个很重要的特性:结果数组的顺序和传入 Promise 的顺序一致,而不是和完成时间一致。
例如:
js
const [tweetsData, weatherData] = await Promise.all([
getTweets(),
getWeather()
]);
虽然 getTweets() 比 getWeather() 更早完成,但结果仍然是:
js
[
tweetsData,
weatherData
]
也就是说,数组第 1 项对应 getTweets(),第 2 项对应 getWeather()。
这也是为什么解构赋值时,变量顺序必须和 Promise 数组顺序保持一致。
Promise.all 的失败处理
Promise.all 还有一个关键规则:只要其中任意一个 Promise 失败,整个 Promise.all 就会失败。
例如:
js
function getWeather() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('天气接口异常');
}, 2000);
});
}
function getTweets() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(['I like cake', 'BBQ is good too!']);
}, 500);
});
}
async function main() {
try {
const result = await Promise.all([
getTweets(),
getWeather()
]);
console.log(result);
} catch (error) {
console.error('至少有一个任务失败:', error);
}
}
main();
当 getWeather() 失败时,Promise.all 返回的 Promise 会变成 rejected,然后进入 catch。
这里也要修正一个常见说法:"只要有一个失败,整体失败,不再等待其他 Promise 执行。"
更准确的说法是:Promise.all 会在第一个 Promise 变成 rejected 时,立刻以这个失败原因进入失败状态,catch 拿到的也是这个失败原因;但已经启动的异步任务并不会自动取消,它们可能仍然会继续执行,只是 Promise.all 不再等待它们的结果。
请求接口时的 Promise.all
再看一个更接近实际开发的例子:同时请求两个接口。
js
const getStory = async () => {
return fetch('https://v1.hitokoto.cn/?c=i&encode=json');
};
const getImage = async () => {
return fetch('https://api.1314.cool/bingimg/?type=json&rand=1');
};
fetch 本身就会返回 Promise,所以这两个函数返回的也是 Promise。
如果两个接口没有依赖关系,可以这样写:
js
async function main() {
try {
const responses = await Promise.all([
getStory(),
getImage()
]);
const data = await Promise.all(
responses.map((response) => response.json())
);
console.log(data);
} catch (error) {
console.error('请求失败:', error);
}
}
main();
这里有两层 Promise.all:
- 第一层等待两个
fetch请求完成,拿到Response对象。 - 第二层等待两个
response.json()解析完成,拿到真正的 JSON 数据。
因为 response.json() 本身也返回 Promise,所以也需要等待。
不推荐混用 await 和 then
有时会看到这样的写法:
js
const res = await Promise.all([getStory(), getImage()])
.then((response) => {
return Promise.all(response.map((res) => res.json()));
})
.catch((response) => {
console.log(response);
});
这段代码能运行,但风格上不够清晰:外层已经使用了 await,里面又继续链式调用 .then() 和 .catch(),读起来会有点绕。
更推荐统一使用 async/await:
js
async function main() {
try {
const responses = await Promise.all([
getStory(),
getImage()
]);
const result = await Promise.all(
responses.map((response) => response.json())
);
console.log(result);
} catch (error) {
console.error(error);
}
}
这样写的好处是执行步骤更直观:
- 先同时发出两个请求。
- 再同时解析两个响应。
- 任意一步失败,都进入
catch。
小结
最后把几个关键点收一下:
- Promise 有三种状态:
pending、fulfilled、rejected。 - Promise 状态只能从
pending变成fulfilled或rejected,并且只能改变一次。 resolve(value)会让 Promise 成功,await可以拿到这个成功值。reject(reason)会让 Promise 失败,await会抛出异常,通常用try...catch处理。- 连续写多个
await,默认是串行等待。 - 多个任务互不依赖时,可以用
Promise.all统一等待。 Promise.all的结果顺序和传入 Promise 的顺序一致。Promise.all遇到第一个失败会立即失败,但不会自动取消已经启动的异步任务。fetch()和response.json()都返回 Promise,所以都需要等待。
理解这些点之后,再看 async/await 和 Promise.all,就不会只停留在"异步变同步"或者"并行执行"这样的模糊印象上了。它们真正解决的是:如何更清晰、更高效地组织异步任务。