目录
回调函数地狱与Promise链式调用
一、回调函数地狱
回调函数地狱是 JavaScript 异步编程早期面临的典型问题,表现为多层嵌套的回调函数,导致代码难以阅读和维护。
1. 典型场景示例
需求:获取默认第一个省,第一个市,第一个地区并展示在下拉菜单中。
使用回调函数实现:
// 1. 获取默认第一个省份的名字
axios({ url: 'http://ajax.net/api/province' })
  .then(result => {
    const pname = result.data.list[0]
    document.querySelector('.province').innerHTML = pname
    // 2. 获取默认第一个城市的名字
    axios({ url: 'http://ajax.net/api/city', params: { pname } })
      .then(result => {
        const cname = result.data.list[0]
        document.querySelector('.city').innerHTML = cname
        // 3. 获取默认第一个地区的名字
        axios({ url: 'http://ajax.net/api/area', params: { pname, cname } })
          .then(result => {
            console.log(result)
            const areaName = result.data.list[0]
            document.querySelector('.area').innerHTML = areaName
          })
      })
  })在回调函数中嵌套回调函数,一直嵌套下去就形成了回调函数地狱。
2. 回调地狱的问题
- 代码金字塔:嵌套层级深,形成“向右倾倒”的金字塔结构,可读性差。 
- 错误处理冗余:每个回调需单独处理错误,代码重复。 
- 流程控制困难:难以实现复杂逻辑(如并行任务、条件分支),耦合性严重。 
二、Promise链式调用
Promise 通过链式调用(Chaining)解决了回调地狱问题,将嵌套结构转为扁平化的流水线式代码。
链式调用:利用 then 方法返回新 Promise 对象特性,一直串联下去。
1. 链式调用解决回调地狱
将上述回调地狱改写为链式调用:
let pname = ''
// 1. 得到-获取省份Promise对象
axios({ url: 'http://hmajax.itheima.net/api/province' })
  .then(result => {
    pname = result.data.list[0]
    document.querySelector('.province').textContent = pname
    // 2. 得到-获取城市Promise对象
    return axios({ url: 'http://hmajax.itheima.net/api/city', params: { pname } })
  })
  .then(result => {
    const cname = result.data.list[0]
    document.querySelector('.city').textContent = cname
    // 3. 得到-获取地区Promise对象
    return axios({ url: 'http://hmajax.itheima.net/api/area', params: { pname, cname } })
  })
  .then(result => {
    const aname = result.data.list[0]
    document.querySelector('.area').textContent = aname
  })
  .catch(error => {
    console.log(error)
  })Promise 链式调用如何解决回调函数地狱?
- then 的回调函数中 return Promise对象,影响当前新 Promise 对象的值。
2. 链式调用的核心规则
- 值传递:每个 - .then()接收前一个 Promise 的结果。
- 返回新 Promise: - .then()回调中可返回新 Promise,继续链式调用。
- 错误冒泡:链中任何位置的错误都会传递到最近的 - .catch()。
三、链式调用深度解析
1. 链式调用本质
每个 .then() 会返回 新的 Promise 对象,其状态由回调函数决定:
- 若回调返回非 Promise 值 → 新 Promise 直接成功( - Fulfilled)。
Promise.resolve(1)
  .then(n => n + 2)      // 返回 3(普通值)
  .then(console.log);    // 输出 3- 若回调返回 Promise → 新 Promise 与其状态同步。 
Promise.resolve(1)
  .then(n => Promise.resolve(n + 2)) // 返回新 Promise
  .then(console.log);                // 输出 3- 若回调抛出错误 → 新 Promise 失败( - Rejected)。
Promise.resolve(1)
  .then(() => { throw new Error('Fail') })
  .catch(console.error); // 捕获错误在 then 回调函数中,return 的值会传给 then 方法生成的新 Promise 对象。
2. 错误处理机制
- 统一捕获:通过一个 - .catch()捕获链中所有错误。
- 中断链式:一旦触发错误,后续 - .then()会被跳过,直接跳转至- .catch()。
- 恢复链式:在 - .catch()后仍可继续- .then()。
四、回调地狱 vs 链式调用
| 特性 | 回调函数 | Promise 链式调用 | 
|---|---|---|
| 代码结构 | 嵌套层级深,可读性差 | 扁平化链式,逻辑清晰 | 
| 错误处理 | 每个回调单独处理,冗余 | 统一通过 .catch()捕获 | 
| 流程控制 | 难以实现复杂逻辑(如并行、条件分支) | 结合 Promise.all、async/await更灵活 | 
| 调试难度 | 堆栈信息不完整,难以追踪 | 错误冒泡机制,堆栈更清晰 | 
| 复用性 | 回调函数耦合度高,复用困难 | 每个 .then()可独立封装,复用性强 | 
五、高级链式技巧
1. 条件分支
fetchUser()
  .then(user => {
    if (user.isVIP) {
      return fetchVIPContent(user.id); // 返回新 Promise
    } else {
      return fetchBasicContent(); // 返回普通值
    }
  })
  .then(content => {
    console.log('内容:', content);
  });2. 并行任务
结合 Promise.all 实现并行:
const fetchUser = axios.get('/api/user');
const fetchPosts = axios.get('/api/posts');
Promise.all([fetchUser, fetchPosts])
  .then(([user, posts]) => {
    console.log('用户:', user.data, '帖子:', posts.data);
  });3. 链式中断
通过返回 Promise.reject() 主动中断链式:
login()
  .then(token => {
    if (!tokenValid(token)) {
      return Promise.reject(new Error('Token 无效')); // 主动中断
    }
    return getUserInfo(token);
  })
  .catch(error => {
    console.error('流程中断:', error);
  });六、总结
- 回调地狱是早期异步编程的痛点,代码臃肿且难以维护。 
- Promise 链式调用通过扁平化结构和错误冒泡机制,极大提升了代码可读性和可维护性。 
- 最佳实践: - 优先使用 Promise 链式替代嵌套回调。 
- 结合 - async/await语法糖进一步简化异步代码。
- 善用 - Promise.all、- Promise.race等工具处理复杂场景。
 
async 和 await
async/await是 JavaScript 处理异步操作的语法糖,基于 Promise 实现,旨在让异步代码的写法更接近同步逻辑,彻底解决回调地狱问题。概念: 在 async 函数内,使用 await 关键字取代 then 函数,等待获取 Promise 对象状态的结果值 。
一、async 函数
- 定义与特性 - 语法:在函数前添加 - async关键字,表示该函数包含异步操作。
- 返回值:始终返回一个 Promise 对象: - 若函数返回非 Promise 值,会自动包装为 - Promise.resolve(value)。
- 若抛出错误,返回 - Promise.reject(error)。
 
 - async function fetchData() { return 'Hello World'; // 等价于 Promise.resolve('Hello World') } fetchData().then(console.log); // 输出 "Hello World"
- 错误处理 - 在 - async函数内部使用- try/catch捕获同步或异步错误。
 - async function fetchWithError() { try { const data = await axios({ url: 'invalid-url' }); } catch (error) { console.error('捕获错误:', error); // 网络错误或 Promise 拒绝 } } fetchWithError()
二、await 表达式
- 作用与规则 - 语法: - await后接一个 Promise 对象(或原始值)。
- 行为: - 暂停当前 - async函数的执行,等待 Promise 完成。
- 若 Promise 成功,返回其解决的值。 
- 若 Promise 拒绝,抛出拒绝的原因(需用 - try/catch捕获)。
 
- 限制: - await只能在- async函数内部使用。
 - async function getUser() { const response = await fetch('/api/user'); // 等待 fetch 完成 const data = await response.json(); // 等待 JSON 解析 return data; }
- 执行顺序 - 同步代码优先: - await不会阻塞函数外的代码。
 - async function demo() { console.log(1); await Promise.resolve(); // 暂停此处,但外部代码继续执行 console.log(2); } demo(); console.log(3); // 输出顺序: 1 → 3 → 2
三、async/await解决回调地狱
将上述回调地狱改写为async/await:
async function getData() {
  try {
    const pObj = await axios({ url: 'http://ajax.net/api/province' })
    const pname = pObj.data.list[0]
    const cObj = await axios({ url: 'http://ajax.net/api/city', params: { pname } })
    const cname = cObj.data.list[0]
    const aObj = await axios({ url: 'http://ajax.net/api/area', params: { pname, cname } })
    const aname = aObj.data.list[0]
    document.querySelector('.province').innerHTML = pname
    document.querySelector('.city').innerHTML = cname
    document.querySelector('.area').innerHTML = aname
  } catch (error) {
    console.log(error)
  }
}
getData()错误处理:
- Promise:依赖 - .catch()或- .then()的第二个参数。
- async/await:使用 - try/catch统一处理同步和异步错误。
四、高级用法
1. 并行执行异步任务
- 顺序执行(效率低): - const user = await fetchUser(); // 先执行 const posts = await fetchPosts(); // 后执行(等待 user 完成)
- 并行执行(效率高): - const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
2. 循环中的 await
- 错误示例(顺序执行,耗时长): - for (const url of urls) { await fetch(url); // 每个请求等待上一个完成 }
- 正确示例(并行触发): - const promises = urls.map(url => fetch(url)); const results = await Promise.all(promises);
3. 顶层 await
- ES2022+ 支持在模块的顶层作用域使用 - await。- // 模块中直接使用 const data = await fetchData(); console.log(data);
五、常见问题与解决方案
- 忘记 await - 现象:函数返回 Promise 而非预期值。 
- 解决:确保异步操作前添加 - await。
 - async function demo() { const data = fetch('/api'); // 错误!缺少 await console.log(data); // 输出 Promise 对象 }
- 未捕获的错误 - 现象:未使用 - try/catch导致未处理的 Promise 拒绝。
- 解决:始终用 - try/catch包裹- await,或在函数调用后加- .catch()。
 - async function riskyTask() { await dangerousOperation(); } riskyTask().catch(console.error); // 捕获未处理的错误
- 性能陷阱 - 现象:不必要的顺序执行降低性能。 
- 解决:合理使用 - Promise.all或- Promise.race优化。
 
六、链式调用 vs async/await
| 特性 | Promise 链式调用 | async/await | 
|---|---|---|
| 代码结构 | 链式 .then(),需处理嵌套 | 类似同步代码,无嵌套 | 
| 错误处理 | 通过 .catch()或链式参数 | 使用 try/catch统一处理 | 
| 底层机制 | 直接操作 Promise 链 | 基于生成器和 Promise 的语法糖 | 
| 可读性 | 简单链式清晰,复杂场景混乱 | 逻辑直观,适合复杂异步流程 | 
| 调试体验 | 错误堆栈可能跨多个 .then() | 错误堆栈更贴近代码行号 | 
七、总结
- 核心优势: - 代码扁平化,更接近同步逻辑的直观性。 
- 错误处理更统一( - try/catch覆盖同步和异步错误)。
 
- 适用场景: - 需要顺序执行的异步任务(如依次请求 A → B → C)。 
- 复杂异步流程(需结合条件判断、循环等)。 
 
- 注意事项: - 避免滥用导致性能问题(如无必要的顺序执行)。 
- 在非模块环境中,顶层 - await需封装在- async函数中。