Promise 入门:理解 async/await 与 Promise.all

Promise 入门:理解 async/await 与 Promise.all

在 JavaScript 里,只要写到网络请求、定时器、文件读取这类异步任务,就绕不开 Promise。很多同学刚开始学的时候,会把 Promiseasync/awaitPromise.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 变成了 fulfilledrejected,状态就固定了,之后不能再变。

需要注意一个小细节:状态名是 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 变成了 rejectedawait 会抛出异常,可以用 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();

执行顺序是:

  1. 调用 getWeather(),开始等待 2 秒。
  2. getWeather() 完成后,才调用 getTweets()
  3. getTweets() 再等待 0.5 秒。
  4. 最后打印结果。

所以总耗时大约是:

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);
  }
}

这样写的好处是执行步骤更直观:

  1. 先同时发出两个请求。
  2. 再同时解析两个响应。
  3. 任意一步失败,都进入 catch

小结

最后把几个关键点收一下:

  • Promise 有三种状态:pendingfulfilledrejected
  • Promise 状态只能从 pending 变成 fulfilledrejected,并且只能改变一次。
  • resolve(value) 会让 Promise 成功,await 可以拿到这个成功值。
  • reject(reason) 会让 Promise 失败,await 会抛出异常,通常用 try...catch 处理。
  • 连续写多个 await,默认是串行等待。
  • 多个任务互不依赖时,可以用 Promise.all 统一等待。
  • Promise.all 的结果顺序和传入 Promise 的顺序一致。
  • Promise.all 遇到第一个失败会立即失败,但不会自动取消已经启动的异步任务。
  • fetch()response.json() 都返回 Promise,所以都需要等待。

理解这些点之后,再看 async/awaitPromise.all,就不会只停留在"异步变同步"或者"并行执行"这样的模糊印象上了。它们真正解决的是:如何更清晰、更高效地组织异步任务。

相关推荐
廖菌1 小时前
DATASPACE3D 产品介绍:面向数字孪生与 Web3D 场景的一体化可视化搭建平台
前端·webgl
敖行客 Allthinker1 小时前
前端调试实战:DevTools调试工具 + 二分定位Bug法全总结
前端·typescript·chrome devtools
暗不需求1 小时前
手写深拷贝:从 JSON 到递归,彻底解决循环引用问题
前端·面试
灏仟亿前端技术团队1 小时前
DOMagic:把 AI 修改绑定到页面节点的前端开发态插件
前端
Lugas1 小时前
浏览器为什么要如此严格地限制跨域请求?
前端·黑客·浏览器
想要成为糕糕手1 小时前
前端异步编程的终极指南:从 Promise 到 async/await,这一次彻底搞懂!
javascript·promise
Cobyte1 小时前
17.响应式系统比对:链表如何实现响应式系统的高效更新
前端·javascript·vue.js
2601_963771371 小时前
Speeding Up Creative Agency Portfolios: A Performance Guide
前端·数据库·php
禅思院1 小时前
AI对话前端从入门到崩溃:一个长对话引发的五层优化战争【二】
前端·前端框架·ai编程