Promise链式调用、async和await

发布于:2025-04-16 ⋅ 阅读:(18) ⋅ 点赞:(0)

目录

回调函数地狱与Promise链式调用

一、回调函数地狱

1. 典型场景示例

2. 回调地狱的问题

二、Promise链式调用

1. 链式调用解决回调地狱

2. 链式调用的核心规则

三、链式调用深度解析

1. 链式调用本质

2. 错误处理机制

四、回调地狱 vs 链式调用

五、高级链式技巧

1. 条件分支

2. 并行任务

3. 链式中断

六、总结

async 和 await

一、async 函数

二、await 表达式

三、async/await解决回调地狱

四、高级用法

1. 并行执行异步任务

2. 循环中的 await

3. 顶层 await

五、常见问题与解决方案

六、链式调用 vs async/await

七、总结


回调函数地狱与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.allasync/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.allPromise.race 等工具处理复杂场景。


   

async 和 await

async/await 是 JavaScript 处理异步操作的语法糖,基于 Promise 实现,旨在让异步代码的写法更接近同步逻辑,彻底解决回调地狱问题。

概念: 在 async 函数内,使用 await 关键字取代 then 函数,等待获取 Promise 对象状态的结果值 。


一、async 函数

  1. 定义与特性

    • 语法:在函数前添加 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"
  2. 错误处理

    • 在 async 函数内部使用 try/catch 捕获同步或异步错误。

    async function fetchWithError() {
      try {
        const data = await axios({ url: 'invalid-url' });
      } catch (error) {
        console.error('捕获错误:', error); // 网络错误或 Promise 拒绝
      }
    }
    fetchWithError()

二、await 表达式

  1. 作用与规则

    • 语法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;
    }
  2. 执行顺序

    • 同步代码优先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);

五、常见问题与解决方案

  1. 忘记 await

    • 现象:函数返回 Promise 而非预期值。

    • 解决:确保异步操作前添加 await

    async function demo() {
      const data = fetch('/api'); // 错误!缺少 await
      console.log(data);          // 输出 Promise 对象
    }
  2. 未捕获的错误

    • 现象:未使用 try/catch 导致未处理的 Promise 拒绝。

    • 解决:始终用 try/catch 包裹 await,或在函数调用后加 .catch()

    async function riskyTask() {
      await dangerousOperation();
    }
    riskyTask().catch(console.error); // 捕获未处理的错误
  3. 性能陷阱

    • 现象:不必要的顺序执行降低性能。

    • 解决:合理使用 Promise.all 或 Promise.race 优化。


六、链式调用 vs async/await

特性 Promise 链式调用 async/await
代码结构 链式 .then(),需处理嵌套 类似同步代码,无嵌套
错误处理 通过 .catch() 或链式参数 使用 try/catch 统一处理
底层机制 直接操作 Promise 链 基于生成器和 Promise 的语法糖
可读性 简单链式清晰,复杂场景混乱 逻辑直观,适合复杂异步流程
调试体验 错误堆栈可能跨多个 .then() 错误堆栈更贴近代码行号

七、总结

  • 核心优势

    • 代码扁平化,更接近同步逻辑的直观性。

    • 错误处理更统一(try/catch 覆盖同步和异步错误)。

  • 适用场景

    • 需要顺序执行的异步任务(如依次请求 A → B → C)。

    • 复杂异步流程(需结合条件判断、循环等)。

  • 注意事项

    • 避免滥用导致性能问题(如无必要的顺序执行)。

    • 在非模块环境中,顶层 await 需封装在 async 函数中。