彻底理解 JavaScript 异步编程


JavaScript 是单线程语言,异步是它处理 I/O 的核心机制。理解异步演进历史,有助于写出更清晰的代码。

第一代:回调函数

最早的异步方案,直接简单,但嵌套深了就成了”回调地狱”:

fs.readFile('a.txt', (err, dataA) => {
  if (err) throw err;
  fs.readFile('b.txt', (err, dataB) => {
    if (err) throw err;
    fs.writeFile('result.txt', dataA + dataB, (err) => {
      if (err) throw err;
      console.log('完成');
    });
  });
});

问题显而易见:错误处理重复、嵌套层级深、逻辑难以追踪。

第二代:Promise

Promise 将异步操作封装成对象,支持链式调用:

readFile('a.txt')
  .then(dataA => readFile('b.txt').then(dataB => [dataA, dataB]))
  .then(([dataA, dataB]) => writeFile('result.txt', dataA + dataB))
  .then(() => console.log('完成'))
  .catch(err => console.error(err));

需要并行执行多个请求时,使用 Promise.all

const [users, posts] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
]);

第三代:async / await

语法糖,让异步代码读起来像同步:

async function processFiles() {
  try {
    const dataA = await readFile('a.txt');
    const dataB = await readFile('b.txt');
    await writeFile('result.txt', dataA + dataB);
    console.log('完成');
  } catch (err) {
    console.error(err);
  }
}

三种方式对比

方式错误处理可读性并行支持
回调每个回调手动处理差(嵌套深)困难
Promise.catch()Promise.all
async/awaittry/catch需配合 Promise.all

常见陷阱

在循环中错误使用 await

// 错误:串行执行,效率低
for (const id of ids) {
  const user = await fetchUser(id);
  users.push(user);
}

// 正确:并行执行
const users = await Promise.all(ids.map(id => fetchUser(id)));

忘记处理 Promise rejection

// 危险:未捕获的 rejection 会导致进程崩溃(Node.js)
fetchData().then(process);

// 安全
fetchData().then(process).catch(handleError);

// 或用 async/await
try {
  const data = await fetchData();
  process(data);
} catch (err) {
  handleError(err);
}

async 函数永远返回 Promise

async function getAge() {
  return 18;
}

// 不能直接这样用
const age = getAge();      // age 是 Promise,不是 18
console.log(age + 1);      // NaN

// 正确
const age = await getAge(); // 18

总结

现代 JavaScript 项目应默认使用 async/await,在需要并行执行多个请求时配合 Promise.all。回调函数仅在对接旧接口时使用,且建议用 util.promisify(Node.js)将其包装成 Promise。

判断标准很简单:如果一个函数需要等待某件事完成,就声明为 async;如果一个值可能还没准备好,就 await 它。