Promise链式调用、async和await

news2025/4/16 5:27:46

目录

回调函数地狱与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 函数中。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2335706.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

React ROUTER之嵌套路由

第一张是需要修改router文件createBrowserRouterd参数数组中的路由关系 第二张是需要在一级路由的index.js中选择二级路由的位置 第一步是在全局的router.js文件中加入新的children属性,如图 第二步是在一级路由的index.js文件中声明outLet组件 默认二级路由 在…

TestNG 单元测试详解

1、测试环境 jdk1.8.0 121 myeclipse-10.0-offline-installer-windows.exe TestNG 插件 org.testng.eclipse 6.8.6.20130607 0745 2、介绍 套件(suite):由一个 XML 文件表示,通过<suite>标签定义,包含一个或更多测试(test)。测试(test):由<test>定义&#xf…

通过python实现bilibili缓存视频转为mp4格式

需要提前下好ffmpeg import os import fnmatch import subprocess Bilibili缓存的视频&#xff0c;*280.m4s结尾的是音频文件&#xff0c;*050.m4s结尾的是视频&#xff0c;删除16进制下前9个0&#xff0c;即为正常音/视频 使用os.walk模块&#xff0c;遍历每一个目录&#xf…

【分享】Ftrans文件摆渡系统:既保障传输安全,又提供强集成支持

【分享】Ftrans文件摆渡系统&#xff1a;既保障传输安全&#xff0c;又提供强集成支持&#xff01; 在数字化浪潮中&#xff0c;企业对数据安全愈发重视&#xff0c;网络隔离成为保护核心数据的关键防线&#xff0c;比如隔离成研发网-办公网、生产网-测试网、内网-外网等。网络…

python每日一练

题目一 输入10个整数,输出其中不同的数,即如果一个数出现了多次,只输出一次(要求按照每一个不同的数第一次出现的顺序输出)。 解题 错误题解 a list(map(int,input().split())) b [] b.append(a[i]) for i in range(2,11):if a[i] not in b:b.append(a[i]) print(b)但是会…

算法思想之前缀和(二)

欢迎拜访&#xff1a;雾里看山-CSDN博客 本篇主题&#xff1a;算法思想之前缀和(二) 发布时间&#xff1a;2025.4.11 隶属专栏&#xff1a;算法 目录 算法介绍核心思想大致步骤 例题和为 K 的子数组题目链接题目描述算法思路代码实现 和可被 K 整除的子数组题目链接题目描述算法…

硬件知识积累 单片机+ 光耦 + 继电器需要注意的地方

1. 电路图 与其数值描述 1.1 单片机引脚信号为 OPtoCoupler_control_4 PC817SB 为 光耦 继电器 SRD-05VDC-SL-A 的线圈电压为 67Ω。 2. 需注意的地方 1. 单片机的推挽输出的电流最大为 25mA 2. 注意光耦的 CTR 参数 3. 注意继电器线圈的 内阻 4. 继电器的开启电压。 因为光耦…

Dockerfile 学习指南和简单实战

引言 Dockerfile 是一种用于定义 Docker 镜像构建步骤的文本文件。它通过一系列指令描述了如何一步步构建一个镜像&#xff0c;包括安装依赖、设置环境变量、复制文件等。在现实生活中&#xff0c;Dockerfile 的主要用途是帮助开发者快速、一致地构建和部署应用。它确保了应用…

MCU屏和RGB屏

一、MCU屏 MCU屏‌&#xff1a;全称为单片机控制屏&#xff08;Microcontroller Unit Screen&#xff09;&#xff0c;在显示屏背后集成了单片机控制器&#xff0c;因此&#xff0c;MCU屏里面有专用的驱动芯片。驱动芯片如&#xff1a;ILI9488、ILI9341、SSD1963等。驱动芯片里…

Elasticsearch 向量数据库,原生支持 Google Cloud Vertex AI 平台

作者&#xff1a;来自 Elastic Valerio Arvizzigno Elasticsearch 将作为第一个第三方原生语义对齐引擎&#xff0c;支持 Google Cloud 的 Vertex AI 平台和 Google 的 Gemini 模型。这使得联合用户能够基于企业数据构建完全可定制的生成式 AI 体验&#xff0c;并借助 Elastics…

蓝桥杯基础数论入门

一.试除法 首先我们要了解&#xff0c;所有大于1的自然数都能进行质因数分解。试除法作用如下&#xff1a; ​质数判断 试除法通过验证一个数是否能被小于它的数&#xff08;一般是用2到用根号x&#xff09;整除来判断其是否为质数。根据定义&#xff0c;质数只能被1和自身整除…

Spring 事件机制与观察者模式的深度解析

一、引言 在软件设计中&#xff0c;观察者模式&#xff08;Observer Pattern&#xff09;是一种非常经典且实用的设计模式。它允许一个对象&#xff08;Subject&#xff09;在状态发生改变时通知所有依赖它的对象&#xff08;Observers&#xff09;&#xff0c;从而实现对象之…

【软考系统架构设计师】信息安全技术基础知识点

1、 信息安全包括5个基本要素&#xff1a;机密性、完整性、可用性、可控性与可审查性。 机密性&#xff1a;确保信息不暴露给未授权的实体或进程。&#xff08;采取加密措施&#xff09; 完整性&#xff1a;只有得到允许的人才能修改数据&#xff0c;并且能够判断出数据是否已…

2025年第十六届蓝桥杯省赛真题解析 Java B组(简单经验分享)

之前一年拿了国二后&#xff0c;基本就没刷过题了&#xff0c;实力掉了好多&#xff0c;这次参赛只是为了学校的加分水水而已&#xff0c;希望能拿个省三吧 >_< 目录 1. 逃离高塔思路代码 2. 消失的蓝宝思路代码 3. 电池分组思路代码 4. 魔法科考试思路代码 5. 爆破思路…

01-算法打卡-数组-二分查找-leetcode(704)-第一天

1 数组基础理论 数组是存放在连续内存空间上的相同数据结构的集合。数组可以通过下标索引快速获取数据&#xff0c;因为数组的存储空间是连续的所以在删除、更新数据的时候需要移动其他元素的地址。 下图是一个数组的案例图形&#xff1a;【内存连续、索引小标从0开始可…

怎么看英文论文 pdf沉浸式翻译

https://arxiv.org/pdf/2105.09492 Immersive Translate Xournal打开

RabbitMQ 深度解析:从基础到高级应用的全面指南

&#x1f430; RabbitMQ 深度解析&#xff1a;从基础到高级应用的全面指南 前言&#x1f4d8; 一、RabbitMQ 简介⚙️ 二、核心特性可靠性 &#x1f512;灵活路由 &#x1f504;高可用性 &#x1f310;多协议支持 &#x1f30d;多语言客户端 &#x1f4bb;插件机制 &#x1f50…

【图灵Python爬虫逆向】题七:千山鸟飞绝

题目背景 题目地址&#xff1a;https://stu.tulingpyton.cn/problem-detail/7/ 这一题为中等难度 打开控制台时会发现进入无限debug&#xff0c;可以通过右键点击"一律不在此处暂停"来绕过这个障碍。 一、请求与响应分析 1. 请求参数分析 首先观察网络请求&…

ubuntu 2404 安装 vcs 2018

参考ubuntu 2204 安装 vcs 2018 系统信息 Ubuntu 24.04.2 LTS ubuntu和 安装后的 vcs 花费了 22G , 其中 "安装后的 vcs" 占13G预先配置 过程 和 2204 安装 vcs 2018 不同, 其他相同 // vm-tools 的安装, 不是虚拟机不需要 sudo apt-get update sudo apt-get inst…