🌟 从回调地狱到 async/await:深入理解 JavaScript 异步编程(ES6 + ES8)

44 阅读3分钟

一、异步演进之路

1. 回调函数(Callback)—— “回调地狱”

js
编辑
fs.readFile('1.txt', 'utf-8', (err, data1) => {
  if (err) throw err;
  fs.readFile(data1.trim(), 'utf-8', (err, data2) => {
    if (err) throw err;
    fs.readFile(data2.trim(), 'utf-8', (err, data3) => {
      // ... 嵌套越来越深
    });
  });
});

✅ 优点:简单直接
❌ 缺点:可读性差、错误处理困难、难以维护


2. ES6 Promise —— 链式调用的曙光

Promise 是一个表示异步操作最终完成或失败的对象

js
编辑
const readFile = (path) => {
  return new Promise((resolve, reject) => {
    fs.readFile(path, 'utf-8', (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
};

readFile('1.txt')
  .then(data => readFile(data.trim()))
  .then(data => readFile(data.trim()))
  .catch(err => console.error(err));

✅ 优点:

  • 链式调用 .then().then()
  • 统一错误处理 .catch()
  • 支持并发 Promise.all()

❌ 缺点:

  • 语法仍显冗长
  • 无法像同步代码一样书写逻辑

3. ES8 async / await —— 异步代码同步化

async/await 是基于 Promise 的语法糖,让异步代码看起来像同步

js
编辑
const main = async () => {
  try {
    const data1 = await readFile('1.txt');
    const data2 = await readFile(data1.trim());
    const data3 = await readFile(data2.trim());
    console.log(data3);
  } catch (err) {
    console.error(err);
  }
};
main();

✅ 优点:

  • 代码简洁、逻辑清晰
  • 错误处理用 try/catch
  • 可配合 for...ofif 等同步控制流

💡 本质await 后面必须是 Promise,async 函数返回一个 Promise。


二、核心对比

特性回调函数Promiseasync/await
可读性差(嵌套深)中(链式)优(同步风格)
错误处理分散.catch() 统一try/catch
并发支持✅ Promise.all()✅ await Promise.all()
调试难度低(堆栈清晰)

现代开发推荐:优先使用 async/await,底层封装用 Promise。


三、实际应用场景

场景 1:浏览器 Fetch API

js
编辑
const fetchData = async () => {
  const res = await fetch('https://api.github.com/users/shunwu/repos');
  const data = await res.json();
  console.log(data); // 数组:用户的所有仓库
};
fetchData();

场景 2:Node.js 文件读取(Promise 化)

js
编辑
import { promises as fs } from 'fs'; // 直接使用 Promise 版本

const readConfig = async () => {
  const content = await fs.readFile('./config.json', 'utf-8');
  return JSON.parse(content);
};

四、大厂高频面试题(附答案)


❓ 面试题 1:async/await 和 Promise 有什么关系?

  • async/await 是 基于 Promise 实现的语法糖
  • async 函数总是返回一个 Promise
  • await 只能用于 async 函数内部,它会暂停函数执行,等待右侧的 Promise settle(fulfilled/rejected),然后继续。
  • 本质上,await p 等价于 p.then(...),但写法更直观。

❓ 面试题 2:下面代码输出什么?为什么?

js
编辑
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);

:输出顺序是 1 → 4 → 3 → 2

原因

  • console.log(1) 和 4 是同步代码,先执行。
  • Promise.then() 属于 微任务(microtask) ,在当前宏任务结束后立即执行。
  • setTimeout 是 宏任务(macrotask) ,要等下一轮事件循环。
  • 事件循环顺序:同步 → 微任务 → 宏任务

❓ 面试题 3:如何用 Promise 实现 sleep(1000)

js
编辑
const sleep = (ms) => {
  return new Promise(resolve => setTimeout(resolve, ms));
};

// 使用
await sleep(1000); // 暂停 1 秒

这是模拟延时的经典方法,常用于测试或节流。


❓ 面试题 4:Promise.all 和 Promise.allSettled 有什么区别?

方法行为适用场景
Promise.all([...])只要有一个 reject,立即 reject需要全部成功才继续(如并行加载资源)
Promise.allSettled([...])等待所有 Promise 结束(无论成功/失败) ,返回 { status, value/reason }需要处理部分失败的情况(如批量请求)

示例:

js
编辑
const results = await Promise.allSettled([
  fetch('/api/user'),
  fetch('/api/posts')
]);
// 即使一个失败,也能拿到另一个的结果

❓ 面试题 5:async 函数中的异常如何捕获?

:两种方式:

方式 1:try/catch

js
编辑
const fn = async () => {
  try {
    await someAsyncTask();
  } catch (err) {
    console.error('捕获异常:', err);
  }
};

方式 2:调用时用 .catch()

js
编辑
fn().catch(err => console.error(err));

⚠️ 注意:如果既不用 try/catch 也不处理返回的 Promise,会导致 未处理的 Promise rejection,可能使程序崩溃(Node.js)或静默失败(浏览器)。


五、总结

  • 回调函数 → 基础但易陷“地狱”
  • Promise(ES6)  → 解决链式调用和错误处理
  • async/await(ES8)  → 让异步代码像同步一样写,现代开发首选

🚀 最佳实践

  • 封装异步操作为返回 Promise 的函数
  • 业务逻辑用 async/await 编写
  • 始终处理错误(try/catch 或 .catch()
  • 并发请求用 Promise.all / allSettled