Promise 与异步抽象演化

第二编 · 第五章:Promise 与异步抽象演化的深入分析


从回调地狱开始

在 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。这些错误在面试中经常出现,值得特别注意。