为什么我用了async/await,代码却没有按序执行

当开发者使用了异步函数(通常指async/await语法)后,发现代码并没有像预想中那样严格地“从上到下”按序执行,其根本原因在于对“异步函数”工作机制的一个核心误解:即错误地,将异步函数中await关键字的“暂停”,等同于了传统同步代码的“阻塞”。一个异步函数,其内在的运行逻辑涵盖了五个关键点:await关键字只能暂停其所在的异步函数、它并未阻塞整个程序的执行、被await的函数本身没有返回一个“承诺”对象、在循环中错误地并行触发了多个异步操作、以及对Promise.all等并行处理工具的误用

为什么我用了async/await,代码却没有按序执行为什么我用了async/await,代码却没有按序执行

具体来说,当解释器在一个异步函数中,遇到await指令时,它仅仅是“暂停”了当前这个异步函数的执行,然后,便立即将控制权“交还”给了主线程,去执行程序中的其他代码。它并不会像一个真正的“阻塞”操作那样,让整个程序都停下来,静静地等待。只有当await后面所跟随的那个异步操作,在未来的某个时刻,真正完成后,解释器,才会将这个被“暂停”的函数,重新放回执行队列,继续其后续的逻辑。

一、核心误区、将“暂停”等同于“阻塞”

要彻底地理解为何异步函数的执行顺序,会与我们的直觉产生偏差,我们必须首先,在概念上,清晰地区分两个核心的行为:“阻塞”与“暂停”。

1. 什么是真正的“阻塞”?

在同步编程模型中,“阻塞”是指,当程序,遇到一个耗时的操作时(例如,一个复杂的、需要大量计算的循环,或一次同步的网络请求),整个程序的执行线程,都会被**完全地、毫无保留地“卡”**在这个操作上。在它完成之前,后续的任何代码,都无法被执行。如果这个过程,发生在网页前端,那么,整个浏览器界面,都将“冻结”,无法响应用户的任何点击或滚动操作。

2. 异步函数的“非阻塞”本质

与之相对,现代JavaScript中的异步函数,其整个设计的“初心”,就是为了避免这种“阻塞”所带来的灾难性用户体验。 异步函数,是建立在“承诺”这一更底层的异步处理机制之上的、一种更优雅的“语法糖”。当JavaScript的解释器,在一个异步函数中,遇到了一个await关键字时,它在后台,实际执行了如下一系列精妙的操作:

它**“暂停”**了当前这个异步函数的执行。

它将这个异步函数剩余的部分,都打包起来,并“注册”到一个被称为“事件循环”的系统中,并告知系统:“请在await后面的这个异步操作,有了结果之后,再回来,继续执行我打包好的这些剩余代码。”

最关键的一步:在完成了这次“注册”之后,解释器,会立即地,将程序的控制权,交还给主线程。

主线程,在“重获自由”后,就可以继续去执行,位于这个异步函数调用之后的、其他所有的同步代码,或者去响应用户的界面操作。

因此,await,仅仅是“暂停”了它所在的那个函数的“内部时间线”,而丝毫没有“阻塞”整个程序的“外部主时间线”。这个“内部暂停,外部继续”的非阻塞特性,正是所有“执行顺序与预想不符”问题的根源。

二、元凶一、await了一个“非承诺”

await关键字,其在语法上的“契约”,是专门用来“等待”一个“承诺”对象的。一个“承诺”对象,是异步操作结果的一个“占位符”。await的作用,就是“解包”,即暂停执行,直到这个“占位符”中,被填入了未来的“真实结果”。

然而,如果我们await了一个非“承诺”的值或一个同步函数,会发生什么?

await一个普通值:例如await 5。解释器,会将其,等价地,处理为await Promise.resolve(5)。即,它会将这个普通值,包装成一个“立即兑现”的承诺。这个操作,本身,并不会产生异步的效果。

真正的“问题”:调用了同步函数,却误以为是异步的JavaScriptfunction longRunningSyncTask() { // 假设这是一个耗时2秒的、纯粹的、同步的复杂计算 let result = 0; for (let i = 0; i < 2000000000; i++) { result += 1; } return result; } async function main() { console.log("开始"); await longRunningSyncTask(); // 错误用法! console.log("结束"); } main(); console.log("这是在main函数调用之后立即执行的代码");

问题分析longRunningSyncTask函数,其本身,是一个纯粹的同步函数,它并没有返回一个“承诺”对象。因此,await关键字,在此处,虽然语法上合法,但完全没有发挥出其“暂停与释放主线程”的异步作用。整个程序,会完全地、阻塞地,等待那个耗时2秒的循环完成后,才会继续向下执行。其输出顺序,将是:“开始” -> (等待2秒) -> “结束” -> “这是在main函数调用之后立即执行的代码”。

与之相对的另一个错误,则是调用了一个真正的异步函数,但却忘记了使用await来等待它。这会导致,这个异步函数,在后台“启动”后,程序,立即,就执行了下一行代码,从而,也造成了“不按序”的错觉。

三、元凶二、循环中的“并发”陷阱

这是在实践中,导致异步函数“不按序执行”的、最常见、也最具迷惑性的“重灾区”

1. 经典的forEach循环问题

场景:我们需要,遍历一个ID列表,并为每一个ID,都去异步地,从服务器,获取其详细信息,并期望,这个获取的过程,是“一个接一个、按顺序”完成的。

错误的代码:JavaScriptconst ids = [1, 2, 3]; async function fetchAndLog() { console.log("循环开始"); ids.forEach(async (id) => { const result = await fetchResource(id); // 错误地在 forEach 中使用 await console.log(`获取到ID ${id} 的结果:`, result); }); console.log("循环结束"); } fetchAndLog();

预期的输出顺序循环开始 -> 获取到ID 1... -> 获取到ID 2... -> 获取到ID 3... -> 循环结束

实际的输出顺序循环开始 -> 循环结束 -> (等待一段时间后,以不确定的顺序) -> 获取到ID 2... -> 获取到ID 1... -> 获取到ID 3...

问题分析forEach方法,在设计上,是一个纯粹的“同步”迭代器。它不认识,也不关心,你传递给它的那个回调函数,是否是一个“异步函数”。它唯一的职责,就是立即地、同步地、毫无停歇地,遍历ids数组中的每一个元素,并为每一个元素,都“启动”一次你传给它的那个异步回调。

它在瞬间,就启动了三次独立的、并行的fetchResource调用。

然后,forEach循环本身,就立即宣告“结束”了。

因此,console.log("循环结束")这行代码,会立即被执行

在未来的某个不确定的时刻,那三个并行的网络请求,会以不确定的顺序,依次返回结果,并触发它们各自的console.log

2. 正确的“串行”循环:使用for...of 要实现“一个接一个”的串行异步循环,我们必须使用一个能够,被await关键字所“暂停”的循环结构。for...of循环,正是为此而生的、最标准的、现代的解决方案

修正后的代码:JavaScriptasync function fetchAndLogInSeries() { console.log("串行循环开始"); for (const id of ids) { const result = await fetchResource(id); // for...of 循环,会在此处,真正地暂停 console.log(`获取到ID ${id} 的结果:`, result); } console.log("串行循环结束"); } 在这个版本中,for...of循环,在每一次的迭代中,都会因为await真正地暂停,直到当前的fetchResource(id)操作完成后,才会进入下一次的迭代。

3. 正确的“并行”处理:使用Promise.all 有时,我们的意图,并非“串行”,而是期望,所有异步操作,都能“同时开始”,然后,我们只需要,**等待它们“全部完成”**后,再进行下一步。

修正后的代码:JavaScriptasync function fetchAndLogInParallel() { console.log("并行处理开始"); // 1. 立即地,发起所有异步操作,并将返回的“承诺”对象,存入一个数组 const promises = ids.map(id => fetchResource(id)); // 2. 使用 Promise.all,来等待“所有”的承诺,都变为“已兑现”状态 const results = await Promise.all(promises); console.log("所有结果都已获取:", results); console.log("并行处理结束"); } 这种模式,在效率上,远高于“串行”循环,因为它允许所有网络请求,都并行地进行。

四、在流程与规范中“防范”

要系统性地,在团队中,根除这类由于对异步机制理解不深而导致的“时序”问题,我们需要建立相应的流程和规范。

建立清晰的异步编程规范:团队的《编码规范》中,必须有专门的章节,来明确地,定义出,在不同场景下(如串行循环、并行处理),所推荐的、标准的异步编程“范式”。

代码审查代码审查,是发现和纠正“异步逻辑”错误的、最重要的“人工防线”。一个有经验的审查者,会特别地,对代码中所有的forEachmap等循环,与async/await的结合使用,保持高度的警惕。

利用静态分析工具:一些配置良好的“静态代码分析”工具,也能够,在一定程度上,检查出一些常见的、不规范的异步编程模式,并给出警告。

常见问答 (FAQ)

Q1: async/awaitPromise.then(),我应该用哪个?

A1: 在任何可以的情况下,都应优先使用async/await。因为它提供的、类似于“同步”代码的线性书写方式,在“可读性”和“错误处理”的简洁性上,都远远优于.then()的链式调用。async/await是建立在“承诺”机制之上的、更高级的抽象。

Q2: 为什么forEach不支持async/await的暂停行为?

A2: 因为forEach方法的“规范”,在async/await出现之前,就已经被确定了。它的设计,就是一个纯粹的、同步的数组迭代器,其内部,并没有,为“等待一个承诺”这样的异步行为,预留任何机制。

Q3: 什么是“事件循环”?它和这个问题有什么关系?

A3: “事件循环”,是JavaScript异步编程模型的底层核心。简单来说,它是一个不断轮询的机制,负责从“任务队列”中,取出那些已经完成的异步操作的“回调函数”,并将其,放回到“主执行栈”中去执行。await的“暂停”,在底层,正是通过将函数的剩余部分,打包并放入这个“任务队列”,来实现的。

Q4: 如何一次性地,并行执行多个异步任务,并等待它们全部完成?

A4: 使用Promise.all()方法。你需要,首先,将所有的异步操作,都发起调用,并将它们返回的“承诺”对象,都收集到一个数组中。然后,将这个“承诺数组”,作为参数,传递给Promise.all()await Promise.all(...),就会返回一个包含了所有异步操作结果的、新的数组。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:百晓生,转转请注明出处:https://www.chuangxiangniao.com/p/638968.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年11月12日 12:46:06
下一篇 2025年11月12日 12:46:15

相关推荐

  • 什么是加密朋克(CryptoPunks)?它们为什么被视为NFT领域的里程碑?

    加密朋克是2017年由Larva Labs创建的首个NFT项目之一,包含10000个独特像素头像,基于以太坊智能合约发行,用户支付Gas费即可申领,后被Yuga Labs收购;其采用算法随机生成,具备五种角色类型与差异化属性,其中外星人类仅9个,稀缺性推高市场价值,部分拍卖价达数百万美元;项目虽非首…

    2025年12月11日
    000
  • Tectum(TET)币是什么?TET币2025年能涨到多少钱一枚?

    tet币是tectum区块链的原生代币,在其生态系统中发挥重要作用,包括治理、质押等。而tectum则是当前市场上速度最快的区块链之一,为用户提供了一个快速、高效、安全的区块链平台,对一般用户有利。简单介绍项目基本信息之后,投资者更想了解代币未来市场,想知道tet币2025年能涨到多少钱一枚?以便调…

    2025年12月11日 好文分享
    000
  • Galaxy分析:以太坊(ETH)基金会遭内部人公开吐槽 EF治理挑战在哪里

    Binance币安 欧易OKX ️ Huobi火币️ 10月17日,以太坊资深研究员Dankrad Feist宣布他将加入 Tempo,这是一条由 Paradigm 开发的、专注于支付的 Layer-1链。Dankrad自 2019 年以来一直在以太坊基金会全职工作(在加密货币领域,六年就像一辈子。…

    2025年12月11日
    000
  • 什么是 StarryNift (SNIFT) 币?功能作用、投资潜力以及未来介绍

    目录 SNIFT 代币的起源与发展什么是 Starry Nift(SNIFT)?谁创建了 Starry Nift (SNIFT)?哪些风险投资公司支持 Starry Nift (SNIFT)?Starry Nift(SNIFT)的工作原理星空人工智能StarryAI SDKDID 公民身份Starr…

    2025年12月11日
    000
  • php DateTime对象如何使用 php DateTime类常用方法指南

    PHP推荐使用DateTime对象而非传统函数,因其提供面向对象、时区管理、错误处理和易读的加减比较操作,显著提升代码可靠性与维护性。 DateTime 对象是 PHP 中处理日期和时间的核心工具,它提供了一种面向对象且强大灵活的方式来管理时间戳、格式化输出、进行时间计算和时区转换,远比传统的 da…

    2025年12月10日 好文分享
    000
  • php如何执行外部命令?php执行系统外部命令详解

    答案是proc_open()最适合处理长时间运行的外部命令并实时获取输出,因其支持非阻塞I/O、精细控制进程的输入输出流,并可通过stream_select()实现多管道监听,实时读取stdout和stderr,同时避免PHP进程完全阻塞,适用于需要持续反馈和交互的复杂场景。 PHP执行外部命令,说…

    2025年12月10日
    000
  • 什么是最终用户许可协议(EULA)和NFT许可?两者在所有权上有何区别?

    EULA规定用户仅获非独占使用权,禁止反向工程与非法使用,软件按“现状”提供,开发者免责,违约可终止协议;NFT许可允许持有者控制代币并自由交易,部分支持商业利用,但版权仍归创作者所有,条款可通过智能合约更新,高价值NFT或附带链外权益;二者核心差异在于EULA仅授使用权且无所有权,依赖中心化执行,…

    2025年12月9日
    000
  • Allora (ALLO)币是什么?工作原理、代币经济学介绍

    allora 是一个自我改进的去中心化人工智能网络,它利用社区构建的机器学习模型进行精准的、情境感知的预测。allora 由 nick emmons 和 kenny peluso 于 2019 年创立,并获得了 polychain capital、framework ventures 和 block…

    2025年12月9日
    000
  • 瑞波币最新价格查询_瑞波币官方网站入口

    瑞波(ripple)是一个旨在连接全球银行、支付提供商和数字资产交易所的开放支付网络,其原生数字货币被称为瑞波币(xrp)。与许多主流加密货币不同,xrp专注于为金融机构提供一种高效、低成本的跨境支付解决方案,凭借其极快的交易确认速度和高度的可扩展性,在全球支付领域展现了巨大的潜力,成为了数字货币市…

    2025年12月9日
    000
  • 瑞波币XRP官网导航 瑞波币App使用入口

    binance币安交易所 注册入口: APP下载: 欧易OKX交易所 注册入口: APP下载: 火币交易所: 注册入口: APP下载: 为了帮助用户准确获取瑞波币(XRP)及其底层技术的相关信息,本文将系统梳理其官方网站的关键入口和移动端应用的使用路径。通过本指南,您可以清晰地了解如何访问核心资源,…

    2025年12月9日
    000
  • 狗狗币价格预测:多头能否引发 0.25 美元的突破?一文分析

    狗狗币(Dogecoin)是什么?值得投资吗? ‍ 狗狗币(Dogecoin)诞生于2013年12月,由软件开发者Billy Markus与Jackson Palmer共同推出,是迷因币(Meme Coin)的鼻祖。 当时两人认为加密货币氛围过于严肃,于是以轻松幽默的心态创造了狗狗币,并采用网络爆红…

    2025年12月9日 好文分享
    000
  • 突然就“推理 Agent 元年”了,再聊 AI Chat 与 AI Agent

    今年 3 月份,我们还在以为 ai agent 的新纪元需要等到“泛 agi”,依靠大模型自身的能力和与之相辅相成的一系列技术的发展,诸如 rag、调用链等,去将大模型的能力更深入地“外置”给 agent 单元体。 然而到了下半年,随着大模型自身推理能力的爆发,以及生态中 MCP、ACP、A2A、上…

    2025年12月6日 行业动态
    000
  • Go语言中枚举的惯用实现方式

    本文深入探讨了Go语言中实现枚举的惯用方法,重点介绍了iota关键字的机制与应用。通过详细的代码示例,文章阐述了iota在常量声明中的重置、递增特性及其在生成系列相关常量时的强大功能,并演示了如何结合自定义类型创建类型安全的枚举,以满足如表示DNA碱基等特定场景的需求。 引言:Go语言中的枚举需求 …

    2025年12月3日 后端开发
    000
  • Go 程序沙盒化:构建安全隔离环境的策略与实践

    本文探讨了 Go 程序沙盒化的核心策略与实践。针对运行不可信 Go 代码的需求,文章阐述了通过限制或伪造标准库包(如 unsafe、net、os 等)、严格控制运行时环境(如 GOMAXPROCS)以及禁用 CGO 和汇编代码等手段来构建安全隔离环境的方法。强调沙盒设计需根据具体安全需求定制,并提醒…

    2025年12月2日 后端开发
    000
  • mysql持续交付如何实现_mysql数据库devops

    将MySQL数据库变更纳入版本控制并使用Flyway等工具管理迁移脚本,实现与应用代码同步;通过CI/CD流水线自动化测试、灰度发布和回滚机制,确保数据库交付高效、安全、可追溯。 在现代软件开发中,MySQL数据库的持续交付(Continuous Delivery)是DevOps实践的重要组成部分。…

    2025年12月2日 数据库
    000
  • Go与C++ DLL互操作:SWIG在Windows平台上的兼容性考量与实践

    本文深入探讨了在Windows环境下使用SWIG将Go语言与C++ DLL集成的挑战,特别是当遇到“adddynlib: unsupported binary format”错误时。核心问题在于SWIG在Windows上对Go语言的DLL绑定,其官方兼容性主要集中在32位系统。文章提供了详细的集成流…

    2025年12月2日 后端开发
    100
  • Go语言编译产物体积探秘:静态链接与运行时机制解析

    Go语言编译的二进制文件体积相对较大,主要源于其默认采用静态链接,将完整的Go运行时、类型信息、反射支持及错误堆栈追踪等核心组件打包到最终可执行文件中。即使是简单的”Hello World”程序也概莫能外,这种设计旨在提供独立、高效且无外部依赖的运行环境。 go语言的设计哲学…

    2025年12月2日 后端开发
    000
  • Go语言日期与时间处理详解:time 包核心机制与实践

    Go语言通过其内置的time包提供了一套强大且精确的日期时间处理机制。它以Time结构体为核心,能够以纳秒级精度表示时间瞬间,且在内部表示中不考虑闰秒。time包依赖IANA时区数据库处理复杂的时区和夏令时规则,确保全球时间信息的准确性。本文将深入探讨Time结构体的设计、时区管理,并提供实际应用示…

    2025年12月2日 后端开发
    000
  • 使用 Go 构建时添加 Git Revision 信息到二进制文件

    在软件开发过程中,尤其是在部署后进行问题排查时,快速确定运行中的二进制文件对应的源代码版本至关重要。本文将介绍一种在 Go 语言构建过程中嵌入 Git Revision 信息的方法,以便在程序运行时方便地获取版本信息。 利用 ldflags 在构建时设置变量 Go 语言的 go build 命令提供…

    2025年12月2日 后端开发
    200
  • 深入理解Go语言gc编译器与C语言调用约定的差异

    Go语言的gc编译器不采用与C语言兼容的调用约定,主要是因为Go独特的协程栈(split stacks)机制使其无法直接与C代码互操作,因此保持调用约定兼容性并无实际益处。然而,gccgo作为Go的另一个编译器实现,在特定条件下可以实现与C语言兼容的调用约定,因为它能支持C语言的栈分割特性,从而提供…

    2025年12月2日 后端开发
    000

发表回复

登录后才能评论
关注微信