Promise 没那么难:从状态流转到 async/await

2022/10/15

第一次接触 Promise 时,thencatch 和一串回调看起来很绕。其实顺着“异步操作什么时候有结果”这条线去理解,它并不复杂:Promise 负责保存未来的成功或失败结果,async/await 则让我们用更接近同步代码的方式读取这个结果。

一、Promise 的三种状态

Promise 有三种状态:

  1. pending:操作还没有完成。
  2. fulfilled:操作成功,得到了结果。
  3. rejected:操作失败,得到了错误原因。

状态只能从 pending 变为 fulfilledrejected。一旦确定,就不能再次改变。

const request = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = true;
    success
      ? resolve('数据加载成功')
      : reject(new Error('网络请求失败'));
  }, 1000);
});

new Promise() 的执行器函数会立即同步执行,真正异步的是里面的定时器、网络请求等任务。resolvereject 负责确定最终状态。

二、链式调用与错误传递

then 会返回一个新的 Promise,所以前一步的返回值可以直接交给下一步。

fetchUser()
  .then(user => fetchOrders(user.id))
  .then(orders => orders.filter(order => order.paid))
  .then(paidOrders => console.log(paidOrders))
  .catch(error => console.error('加载失败:', error))
  .finally(() => console.log('请求结束'));

如果 then 返回普通值,下一步会接收到这个值;如果返回另一个 Promise,链条会等待它完成;如果抛出异常,错误会进入后面的 catch。常见问题是忘记 return:异步任务没有被返回时,后面的步骤就不会等待它。

三、多个异步任务怎么组合

处理多个任务时,根据业务能否接受部分失败来选择方法:

  1. Promise.all:全部成功才返回,任意一个失败就整体失败。
  2. Promise.allSettled:等待全部结束,并保留每一项的成功或失败状态。
  3. Promise.any:返回第一个成功结果,全部失败才拒绝。
  4. Promise.race:返回第一个完成的结果,不区分成功或失败。
const results = await Promise.allSettled([
  fetchProfile(),
  fetchMessages(),
  fetchNotifications()
]);

const successful = results
  .filter(item => item.status === 'fulfilled')
  .map(item => item.value);

例如首屏核心数据缺一不可时可以使用 Promise.all;多个独立模块分别加载时,Promise.allSettled 更适合保留部分成功结果。

四、Promise 与微任务

thencatchfinally 的回调属于微任务。当前同步代码执行完后,微任务会先于 setTimeout 这类宏任务执行。

console.log(1);
Promise.resolve().then(() => console.log(2));
setTimeout(() => console.log(3));
console.log(4);
// 1、4、2、3

这里先输出同步代码 1 和 4,再执行微任务输出 2,最后执行定时器输出 3。理解这个顺序,有助于排查请求回调、页面更新和定时器之间的问题。

五、async 和 await 分别做了什么

async 用来声明异步函数。被 async 修饰的函数一定会返回 Promise:函数直接返回的普通值会被包装成成功结果,函数内部抛出的异常则会变成失败结果。

async function getName() {
  return 'Tess';
}

getName().then(name => console.log(name)); // Tess

await 用来等待一个 Promise 的结果。它会暂停当前 async 函数后续代码的执行,但不会阻塞浏览器主线程。Promise 成功时,await 得到成功值;Promise 失败时,它会抛出异常,所以通常需要配合 try...catch

async function loadPage() {
  try {
    const user = await fetchUser();
    const orders = await fetchOrders(user.id);
    return orders;
  } catch (error) {
    console.error('加载失败:', error);
    throw error;
  }
}

await 通常只能写在 async 函数中(ES Module 顶层 await 除外)。还要注意,连续写多个 await 会让任务按顺序执行。如果多个请求互不依赖,可以先创建 Promise,再用 Promise.all 并行等待:

const [profile, messages] = await Promise.all([
  fetchProfile(),
  fetchMessages()
]);

所以,async 负责让函数返回 Promise,await 负责取出 Promise 的结果。它们让异步流程更容易阅读,但 Promise 的状态、错误传递和微任务规则仍然存在。