【JS 异步】告别回调地狱:Async/Await 和 Promise 的优雅实践与错误处理

【JS 异步】告别回调地狱:Async/Await 和 Promise 的优雅实践与错误处理

所属专栏: 《前端小技巧集合:让你的代码更优雅高效
上一篇: 【JS 数组】数组操作的“瑞士军刀”:精通 Array.reduce() 的骚操作
作者: 码力无边


引言:那座名为“回调地狱”的金字塔,我们曾亲手搭建

嘿,各位在代码世界里追求光与热的道友们,我是码力无边

在我们的前端江湖中,如果说 DOM 操作是“外家功夫”,数据处理是“内功心法”,那么异步编程,就是那门决定你能否“御剑飞行”的“轻功”。因为在 Web 世界,几乎所有有价值的操作都是异步的:

  • 向服务器请求数据(Ajax/Fetch)
  • 读取本地文件
  • 设置一个定时器 (setTimeout)
  • 等待用户点击一个按钮

这些操作都不会立即返回结果。JavaScript 作为一门单线程语言,为了不被这些耗时的任务阻塞主线程(否则页面就会卡死),它采用了“稍后处理”的异步模型。而在远古时代,我们实现这种“稍后处理”的唯一方式,就是回调函数 (Callback)

让我们一起瞻仰一下那座由我们亲手搭建,又让我们备受折磨的“史前遗迹”——回调地狱 (Callback Hell),又称“毁灭金字塔 (Pyramid of Doom)”:

// 史前遗迹,请勿模仿
ajax('api/user/1', function(user) {console.log('获取到用户:', user);ajax(`api/posts?userId=${user.id}`, function(posts) {console.log('获取到帖子:', posts);ajax(`api/comments?postId=${posts[0].id}`, function(comments) {console.log('获取到评论:', comments);// 如果还有下一步...天啊...ajax(`api/replies?commentId=${comments[0].id}`, function(replies) {console.log('获取到回复:', replies);// 金字塔已经高耸入云...}, function(error) {// 每一层都要处理错误...});}, function(error) {// ...});}, function(error) {// ...});
}, function(error) {// ...
});

这种代码,就像一个向右无限延伸的俄罗斯套娃。它的问题显而易见:

  • 可读性极差:代码逻辑不是从上到下,而是像贪吃蛇一样扭曲。
  • 难以维护:想在中间加一步?或者修改某一步的逻辑?祝你好运。
  • 错误处理复杂:每一层嵌套都需要单独处理错误,很容易遗漏。

为了推翻这座压迫我们多年的“金字塔”,JavaScript 社区的先贤们进行了不懈的斗争,最终为我们带来了两件划时代的法宝:Promiseasync/await

今天,码力无边就将带你走过这条从“地狱”到“天堂”的救赎之路,让你彻底掌握现代 JavaScript 中最优雅、最强大的异步编程范式。

一、Promise:从“回调”到“承诺”的革命

Promise 的出现,是异步编程思想的一次伟大飞跃。它把“传递一个函数进去等待执行”的模式,变成了“给你一个承诺对象,你拿着它等结果”的模式。

一个 Promise 对象,就像一张“彩票”。你买下它的时候,它处于 pending (进行中) 状态。未来,它可能会中奖,变成 fulfilled (已成功) 状态,并给你奖金(结果值);也可能没中奖,变成 rejected (已失败) 状态,并告诉你原因(错误信息)。

1.1 用 .then() 链式调用,拆解金字塔

Promise 最核心的变革,就是引入了 .then() 方法。.then() 方法可以接收两个函数作为参数,一个用于处理成功状态,一个用于处理失败状态。更重要的是,.then() 方法会返回一个新的 Promise 对象,这使得我们可以进行链式调用

让我们用 Promise 来重构上面的“地狱”代码:

// 假设 ajax 函数现在返回一个 Promise
ajax('api/user/1').then(user => {console.log('获取到用户:', user);// 返回一个新的 Promisereturn ajax(`api/posts?userId=${user.id}`); }).then(posts => {console.log('获取到帖子:', posts);// 返回又一个新的 Promisereturn ajax(`api/comments?postId=${posts[0].id}`);}).then(comments => {console.log('获取到评论:', comments);return ajax(`api/replies?commentId=${comments[0].id}`);}).then(replies => {console.log('获取到回复:', replies);}).catch(error => {// 革命性的改变:用一个 .catch() 捕获链条上任何一个环节的错误!console.error('发生错误:', error);});

看到了吗?金字塔被夷为平地!代码变成了从上到下的线性结构,逻辑清晰无比。

  • 线性流程:每一步操作都清晰地写在一个 .then() 中。
  • 统一错误处理:链式调用中任何一个 Promise 变成 rejected,都会被最后的 .catch() 捕获。告别了层层嵌套的错误处理。
1.2 Promise 的“静态方法”:Promise.allPromise.race

Promise 还提供了一些强大的工具函数:

  • Promise.all(iterable): 并行执行,等待所有

    • 场景:你需要同时请求用户基本信息、用户的好友列表和用户的相册,三者没有依赖关系,但你希望等它们全部成功后,再渲染页面。
    • 用法:它接收一个 Promise 数组,返回一个新的 Promise。只有当数组中所有的 Promise 都成功时,它才会成功,并且结果是一个包含所有 Promise 结果的数组。如果其中任何一个失败了,它就会立刻失败。
    Promise.all([ajax('api/userInfo'),ajax('api/friendList'),ajax('api/album')
    ]).then(([userInfo, friendList, album]) => {// 在这里,三个请求都已成功完成renderPage(userInfo, friendList, album);
    }).catch(error => {// 只要有一个请求失败,就会进入这里showErrorPage(error);
    });
    
  • Promise.race(iterable): 并行执行,谁快用谁

    • 场景:你向两个不同的 CDN 节点请求同一个资源,哪个先返回就用哪个。或者,给一个请求设置超时:让你的请求和 setTimeout 返回的 Promise 赛跑。
    • 用法:它也接收一个 Promise 数组,但只要其中任何一个 Promise 率先改变状态(无论是成功还是失败),它就会立即采用那个 Promise 的状态和结果。
    function requestWithTimeout(url, timeout) {let timeoutPromise = new Promise((_, reject) => {setTimeout(() => reject(new Error('请求超时!')), timeout);});return Promise.race([fetch(url),timeoutPromise]);
    }requestWithTimeout('api/slow-resource', 3000).then(response => console.log('请求成功:', response)).catch(error => console.error(error.message)); // 可能是网络错误,也可能是“请求超时!”
    

Promise 已经非常强大了,但它仍然需要 .then() 的回调函数语法。人类的大脑,终究还是更习惯同步的、阻塞式的代码写法。于是,终极形态的“异步救世主”登场了。

二、async/await:用写同步代码的方式,来写异步

async/await 是 ES2017 (ES8) 引入的,它并不是一个新东西,而是建立在 Promise 之上的语法糖 (Syntactic Sugar)。它的目标只有一个:让异步代码看起来、写起来都像同步代码

两个关键词:

  • async: 用来修饰一个函数,表明这个函数是一个异步函数。任何 async 函数的返回值,都会被自动包装成一个 Promise。
  • await: 只能用在 async 函数内部。它后面通常跟着一个 Promise。它的作用是**“暂停”当前 async 函数的执行,等待后面的 Promise 状态变为 fulfilled,然后直接返回 Promise 的结果值**。如果 Promise 失败了,它会抛出 (throw) 错误。
2.1 终极进化:最“人类友好”的异步代码

让我们用 async/await 来重写我们最初的那个例子,你将见证代码的可读性如何达到巅峰:

async function fetchAllData() {try {// 代码像同步一样,从上到下执行const user = await ajax('api/user/1');console.log('获取到用户:', user);const posts = await ajax(`api/posts?userId=${user.id}`);console.log('获取到帖子:', posts);const comments = await ajax(`api/comments?postId=${posts[0].id}`);console.log('获取到评论:', comments);const replies = await ajax(`api/replies?commentId=${comments[0].id}`);console.log('获取到回复:', replies);return replies; // async 函数的返回值} catch (error) {// 同样革命性的改变:用标准的 try...catch 来捕获所有 await 的错误!console.error('发生错误:', error);}
}fetchAllData().then(result => {console.log('所有数据获取完毕:', result);
});

震撼吗?

  • 完全同步的写法:没有 .then,没有回调,代码的执行顺序和你的阅读顺序完全一致。
  • 标准的错误处理try...catch 是我们再熟悉不过的同步代码错误处理机制,现在它完美地适用于异步流程。任何一个 await 的 Promise 失败,都会被 catch 块捕获。
  • 优雅的返回值async 函数的返回值就是一个 Promise,你可以继续在外部用 .then() 来处理最终的结果。

async/await 同样能和 Promise.all 等工具完美结合:

async function fetchParallelData() {try {const [userInfo, friendList, album] = await Promise.all([ajax('api/userInfo'),ajax('api/friendList'),ajax('api/album')]);renderPage(userInfo, friendList, album);} catch (error) {showErrorPage(error);}
}

三、现代异步编程最佳实践

  1. 优先使用 async/await:在任何可以使用它的地方(现代浏览器、Node.js、或经过 Babel 等工具编译的环境),async/await 都应该是你的首选。它的可读性和可维护性是无与伦比的。

  2. 不要忘记 Promise.all:在使用 async/await 时,要警惕一种反模式——串行执行本可以并行的任务。

    // 反模式:不必要的串行等待
    async function getTwoThings() {const thing1 = await fetchThing1(); // 等待 thing1const thing2 = await fetchThing2(); // thing1 好了才开始请求 thing2return [thing1, thing2];
    }// 正确模式:并行执行
    async function getTwoThingsInParallel() {const [thing1, thing2] = await Promise.all([fetchThing1(),fetchThing2()]);return [thing1, thing2];
    }
    
  3. 顶层 await:最新的 JavaScript (ES2022) 已经支持在模块的顶层使用 await,无需包裹在 async 函数中。这在一些初始化脚本中非常有用。

写在最后:从驯服异步,到驾驭异步

从回调地狱的混乱,到 Promise 链的秩序,再到 async/await 的优雅,JavaScript 的异步编程演进史,就是一部不断追求“人性化”和“可读性”的奋斗史。

掌握 async/await 和 Promise,你就不再是那个被异步任务牵着鼻子走的“回调奴隶”,而是一个能够从容地编排、组织、驾驭复杂异步流程的“时间管理者”。你的代码将不再是难以理解的“面条”,而是结构清晰、逻辑顺畅的“诗篇”。

所以,道友们,请彻底告别回调地狱吧。在你的下一个项目中,大胆地拥抱 async/await,用最现代、最优雅的方式,去驯服时间,驾驭异步!


专栏预告与互动:

我们已经掌握了现代 JS 的异步核心。但代码写得再优雅,也得简洁。ES6 引入了许多强大的“语法糖”,它们能让你用更少的代码,做更多的事。

下一篇,我们将深入 ES6+ 的代码整洁之道,探索解构赋值和展开语法的 5 个神仙用法,让你的代码瞬间“瘦身”,可读性翻倍!

码力无边的异步心法,你 Get 了吗?点赞、收藏、关注,用你的三连,为我的下一次“瞬移”积蓄能量!

今日思考题: forEach 循环是同步的,如果我在 forEach 的回调里使用 await,会发生什么?它会按顺序等待每一个 await 完成吗?为什么?这是一个经典的 async/await 陷阱,把你的分析写在评论区,我们一起探讨!

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

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

相关文章

23.Linux : ftp服务及配置详解

Linux : ftp服务及配置详解 FTP 基本概念 定义:文件传输协议(File Transfer Protocol),采用 C/S 模式工作。端口: 控制端口:21数据端口:20FTP 工作原理模式工作流程连接发起方主动模…

悲观锁乐观锁与事务注解在项目实战中的应用场景及详细解析

在今天做的项目练习部分中真的学到了很多东西,也补充了许多之前遗漏或是忘记的知识点,但时间精力有限,我就先记录一下今天用到的一个新东西,悲观锁和乐观锁。首先给出实际应用背景:在加入锁和事务注解之前,…

Java构造器与工厂模式(静态工程方法)详解

1. 构造器1.1 构造器的核心意义1.1.1 对象初始化构造器在创建对象 (new) 时自动调用, 用于初始化对象的状态 (如设置字段初始值, 分配资源等)无构造器时: 字段为默认值(0/null/false)有构造器:确保对象创建后即处于有效状态1.1.2 强制初始化…

解决jdk初始化运行,防火墙通信选错专业网络问题

问题描述新项目添加不同版本的jdk,运行时提示防火墙通信策略,选成专用网络。其他人访问后端接口时,提示连接失败。 解决方案:1、在搜索栏中输入 防火墙关键字,选择到防火墙和网络保护2、选择允许应用通过防火墙3、先点…

【Linux】常用命令(三)

【Linux】常用命令(三)1. export1.1 原理1.2 常用语法1.3 示例1.4 书中对命令的解释1.5 生效范围2. 测试服务地址与其端口能否访问2.1 nc(Netcat)命令2.2 telnet2.3 nmap2.4 curl命令 (适用于HTTP/HTTPS 服务)1. export export 是 Linux Shell&#xff…

Pytest项目_day15(yaml)

YAMLYAML是一个对所有编程语言都很友好的数据序列化标准,它是一种直观的能够被电脑识别的数据序列化格式,是一种可读性高且容易被人类阅读的脚本语言YAML语言的本质是一种通用的数据串行化格式适用场景 可以直接序列化为数组、字典解析成本低专门写配置文…

审批流程系统设计与实现:状态驱动、灵活扩展的企业级解决方案

审批流程系统设计与实现:状态驱动、灵活扩展的企业级解决方案 本文基于实际企业级审批系统源码,深入解析如何设计高扩展性、强一致性的审批流程引擎,涵盖状态机设计、多租户隔离、文件服务集成等核心实现。 1. 系统设计概览 审批系统的核心架…

汽车免拆诊断案例 | 2010款奥迪A4L车行驶中发动机偶尔自动熄火

故障现象 一辆2010款奥迪A4L车,搭载CDZ发动机 ,累计行驶里程约为18.2万km。该车行驶中发动机偶尔自动熄火,有时熄火后能够立即重新起动着机,有时需要等待一会儿才能重新起动着机,故障频率较低。因该故障在其他维修厂陆…

Liam ERD:自动生成美观的交互式实体关系图

Liam ERD 是一个可以快速生成美观且具有交互性的数据库实体关系图(ERD)的工具,可以帮助用户实现复杂数据库结构的可视化。 Liam ERD 是一个免费开源的项目,代码托管在 GitHub: https://github.com/liam-hq/liam 功能…

网络协议序列化工具Protobuf

目录前言一、下载注意二、解压安装三、Protobuf的使用1、创建.proto文件2、利用protoc编译.proto文件前言 Protocol Buffers是Google的⼀种语⾔⽆关、平台⽆关、可扩展的序列化结构数据的⽅法,它可⽤于(数据)通信协议、数据存储等。 Protoco…

从表单校验到API网关:全链路输入安全防护指南

从表单校验到 API 网关:全链路输入安全防护指南 在软件系统的安全防御体系中,输入安全是第一道防线,而这道防线的坚固程度直接决定了系统抵御外部攻击的能力。从用户在浏览器中填写表单的那一刻起,到数据经过 API 网关流转至后端服务,每一个环节都可能成为输入攻击的突破…

Flask vs Django:微框架与一站式对决

Flask 简介 1、简介 Flask诞生于2010年,是Armin ronacher用Python语言基于Werkzeug工具箱编写的轻量级Web开发框架,又称之为微框架。 "微"的含义:Flask旨在保持核心简洁,本身相当于内核,其他功能需通过扩展…

真实业务场景:mysql慢查询优化(从17秒的查询优化到700毫秒)

慢查询业务场景:原先在我们系统中要统计一些人员的单位 部门信息的数据情况,比如总的男女人数,每个单位下的男女人数等等,然后原来的sql是这样写的 根据一个单位的id 然后对一张表做出多个子查询进行查询,这时候统计记录 由于加载…

远程影音访问:通过 cpolar 内网穿透服务使用 LibreTV

文章目录前言【视频教程】1.关于LibreTV2.docker部署LibreTV3.简单使用LibreTV4.安装cpolar内网穿透5.配置ward公网地址6.配置固定公网地址总结LibreTV 与 cpolar 的协同应用,为用户打造了一条通往高清观影自由的便捷之路。通过这一方案,用户不仅摆脱了商…

Apache ECharts 6 核心技术解密 – Vue3企业级可视化实战指南

简介 ECharts 是百度开源的一个使用 JavaScript 实现的开源可视化库,它能够生动、可交互地展示数据。在 Vue3 项目中集成 ECharts 可以让你的项目更加直观和动态地呈现数据信息。 核心优势 特性SVG渲染器Canvas渲染器缩放保真度★★★★★★★☆☆☆动态交互性能…

考公VS考研,拼哪个性价比高?

即将到来下半年,将迎来考公和考研是两个非常重要的考试,也是许多年轻人为之奋斗的目标。无论是获得一份稳定的“铁饭碗”,还是提升学历学位获得更高的竞争力,都是值得努力的方向。那么,考公vs考研,到底哪个…

python2操作neo4j

环境依赖 jdk、neo4j图数据库 操作一条数据完整demo import os,json,sys,io from py2neo import Graph,Nodetry:sys.stdout io.TextIOWrapper(sys.stdout.buffer, encodingutf-8)sys.stderr io.TextIOWrapper(sys.stderr.buffer, encodingutf-8) except Exception:passcla…

AI 编程实践:用 Trae 快速开发 HTML 贪吃蛇游戏

1. 背景与目标 贪吃蛇是最适合入门的 2D 网页小游戏之一:规则简单、反馈清晰、可扩展空间大(穿墙模式、道具、多食物、排行榜……)。 demo地址:https://game.haiyong.site/snake-game.html 本项目的目标是: 纯前端、…

FreeRTOS-C语言指针笔记

文章目录一级指针指针基本概念指针使用示例代码说明二、二级指针二级指针重点解析一级指针 C语言中的指针是一个非常重要的概念,它存储了变量的内存地址。指针的使用可以使程序更加高效,尤其在处理数组、字符串和动态内存分配时。 指针基本概念 指针变…

界面布局智能建议生成:从功能需求到专业UI的AI加速之路

内容简介: 传统界面设计让产品经理陷入"不懂设计、等设计师"的困境,效率低下还容易被挑刺。本文深度解析DeepSeek驱动的界面布局智能生成技术,通过DESIGN框架提示词模板,让产品经理在30分钟内生成3种专业级界面方案,实现…