JavaScript 取消 async/await 的真相:你大多只是在忽略结果
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
摘要每位JavaScript开发者都会问同样的问题:为什么我不能直接取消这个异步操作? 当用户离开、组件卸载、新的请求取代旧的请求时——肯定有办法停止不再需要的工作吧? 在实践中,我们采用熟悉的模式:用Promise.race()实现超时,当结果最终到达时忽略它,或者配置AbortController然后认为问题已经解决。这种方法通常看起来有效,直到应用程序开始泄漏资源、执行迟来的副作用,或者在负载下表现不一致。 根本的、核心问题是:「JavaScript不提供任务取消作为原语」。一旦异步工作被调度,就没有强制停止它的通用机制。Promise、回调和异步函数代表的是结果和延续,而不是底层执行的所有权。 这造成了意图与现实之间的不匹配:开发者思考的是"停止工作",但语言的操作方式是让工作运行至完成,并可选择地对其结果做出反应。因此,许多所谓的取消技术只是停止等待结果,而不是停止工作本身。 理解这一差距至关重要,因为它解释了JavaScript的许多异步行为:为什么Promise不能被取消,为什么超时不会停止执行,以及为什么AbortController被设计为信令机制而非终止开关。一旦这个模型清晰,围绕取消的限制就不再感觉是偶然的——它们直接来源于JavaScript如何执行代码。 取消 vs 超时 vs 失败JavaScript中取消经常被误解的原因之一是,它与两个截然不同的概念混淆在一起:超时和失败。这三者都可能导致"这个操作没有产生值",但它们描述的是根本不同的情况。 取消:"我不再需要这个"取消是一个外部决定。操作本身可能完全健康且能够完成,但外部的某些东西——用户输入、应用程序状态、导航或新的请求——使得结果变得不重要。 重要的是,取消不涉及正确性。操作没有失败。只是被要求停止,因为它的结果不再需要。 在设计良好的系统中,取消是预期的和常规的,而非例外的。 超时:"我停止等待了"超时不会取消工作。它只是限制调用者愿意等待结果的时间。 在JavaScript中,超时通常使用Promise.race()实现: 当超时赢得竞赛时,等待代码恢复执行,但 如今,大多数现代API接受AbortSignal。这改善了资源清理和意图信号传递,但它没有改变根本模型:中止仍然是协作式的,只影响选择加入的代码。 这种区别很容易被忽视,因为调用者重新获得控制权,造成工作已经停止的错觉。实际上,超时只是停止了观察结果。 失败:"出错了"失败描述内部问题:网络错误、无效输入、逻辑错误、不可用资源。它们通常表示为拒绝的Promise或抛出的错误。 与取消不同,失败不是故意的。它们表示操作即使在仍然需要其结果的情况下也无法成功完成。 将取消视为失败通常会导致笨拙的错误处理。代码开始捕获"错误",这些根本不是错误,或者抑制失败,因为它们可能只是取消。随着时间的推移,真正的失败变得难以从正常的控制流中区分出来。 为什么这个区别很重要在JavaScript API中,超时和失败经常被重载以代替取消。这在表面上可行,但它掩盖了意图,并将责任推给调用者去猜测实际发生了什么。 一旦你分离了这些概念,一个模式就出现了:JavaScript擅长表达等待和失败,但它没有停止工作的内置概念。所有看起来像取消的东西要么是超时、被忽略的结果,或者是顶层协作协议。 为什么Promise不能被取消当开发者问为什么在JavaScript中取消如此困难时,他们通常的意思是:为什么我不能取消Promise?毕竟,Promise是async/await的基础,大多数异步工作都是用它们表达的。如果Promise代表"任务",取消似乎应该很简单。 但Promise从未被设计用来建模任务。 Promise代表结果,而非执行Promise是未来可用值的占位符。它没有说明该值是如何产生的,甚至没有说明是否有与之相关的正在进行的工作。当你获得一个Promise时,底层操作可能已经完成、正在进行中,或与其他消费者共享。 这种区别很微妙但很关键:Promise不拥有导致它的工作。 一旦创建,Promise必须最终解决——要么 fulfilled,要么 rejected。没有"放弃"或"取消"的第三态,因为这会破坏Promise做出的核心保证:如果你有一个对它的引用,你可以可靠地附加处理器并最终观察到一个结果。 "取消Promise"的谬论想象一下Promise上假设的 考虑这个: 如果一个消费者调用 这些问题在没有引入全局副作用的情况下没有一致的答案。Promise是有意可共享和可组合的,取消会使它们的行为取决于谁还在观察它们。 这就是为什么取消不适合作为Promise本身的方法。取消是关于控制工作,而Promise是关于观察结果。 如果Promise可取消,什么会崩溃使Promise可取消会波及整个异步生态系统:
换句话说,取消会在原本独立的代码片段之间引入隐藏的耦合。 为什么取消必须存在于别处早期的库实验过可取消的Promise,这个想法甚至出现在早期标准化讨论中。结论是一致的:取消不是Promise的属性,而是调用者和被调用者之间的协议。 该协议需要一个单独的通道:可以传递、观察和采取行动的东西——而不破坏Promise本身的语义。这就是为什么现代JavaScript将取消建模为信号,而不是对Promise的操作。 一旦你将Promise视为对未来值的不可变视图,而不是对运行任务的句柄,它们缺乏取消就不再看起来像遗漏。这是一个保持异步代码可预测和可组合的边界。 AbortController到底是什么如果Promise不能被取消,我们如何在JavaScript中实际停止或控制异步工作?这就是理解AbortController真正做什么的关键——以及它不能做什么——对于设计取消感知代码至关重要。 AbortController作为信令机制AbortController本质上是一个信使。它允许一段代码通知其他代码任务应该不再继续。它通过AbortSignal这样做: 在这里, AbortController能做什么
本质上,AbortController提供了一个协作取消协议。消费者必须选择加入并决定如何响应。 AbortController不能做什么
按设计,Abort是协作式的AbortController的协作性质是有意为之的:
例如,考虑一个长时间运行的计算: 没有明确检查 资源清理 vs 任务终止JavaScript取消中的一个常见误解是,认为信号通知任务中止会自动停止所有工作。实际上,停止任务和清理资源之间有一个关键区别,理解这一点对于编写健壮的异步代码至关重要。 停止工作 vs 清理当你对AbortController调用
这就是"资源清理"的含义:系统确保套接字、内存缓冲区或文件描述符不会被留下悬挂。清理对于防止内存泄漏、连接耗尽或其他细微错误至关重要。 然而,资源清理不会自动停止所有正在进行的任何工作。任何CPU密集型计算、同步逻辑或协作API之外的代码都会继续运行,直到自然完成。 为什么JavaScript专注于清理,而非终止JavaScript的执行模型强制运行至完成:一旦函数开始,它将运行到当前同步块的结束。事件循环不允许抢占式中断。结果是:
相反,JavaScript强调协作模式,其中代码自愿检查取消并干净地退出。AbortController符合这个模型:它发出意图,API或函数决定如何响应。 AbortController作为清理触发器大多数支持AbortSignal的现代API专注于资源的干净终止: 在这里,stream可能停止产生数据、关闭内部缓冲区并释放文件描述符。任何消费代码都可以注意到中止并停止进一步处理。工作不会被强制终止:相反,API和调用者协作安全地退出。 要停止CPU密集型任务或自定义计算,开发者必须定期检查 这种清理+协作退出的组合是JavaScript提供的取消模式。它在允许开发者回收资源并优雅地停止长时间运行的操作的同时,保留了安全性。 为什么JavaScript不能强制停止代码JavaScript中的取消与其他语言工作方式不同的原因之一是语言执行代码的方式。理解这一点是实现AbortController不能神奇地"杀死"函数或Promise的关键。 JavaScript中没有抢占JavaScript运行在单线程事件循环上。每个函数在执行下一个任务之前运行至完成: 当 为什么强制终止不安全想象一下如果JavaScript允许任意终止:
因为JavaScript鼓励共享对象和可组合的异步代码,抢占式终止本质上是不安全的。 为什么Web Workers从根本上不能改变这一点一些开发者认为:"我可以在Web Worker中运行CPU工作并终止它。"技术上,你可以: 但这是进程级终止,而非任务级取消:
Web Workers提供了一种隔离可能需要强制杀死的任务的方法,但在主线程内,JavaScript仍然无法安全地抢占代码。这就是为什么像AbortController这样的协作信号是首选模式:它们让代码在清理资源的同时自愿退出。 其他语言如何建模取消JavaScript的协作取消模型可能感觉受限,但看看其他语言有助于解释原因。不同的环境在安全性、控制性和可组合性之间做出不同的权衡。 协作取消(Go、Rust异步)像Go和Rust这样的语言提供显式的协作取消机制: 「Go:上下文传播」 ctx被显式传递给所有可能需要取消的函数。工作本身检查上下文并提前退出。资源可以以结构化的方式清理。 这在概念上类似于JS中的AbortController:一个信号沿调用链传递,需要协作。 「Rust:异步取消」 Rust中的Future可以用取消信号轮询。任务在控制点让出控制,如果信号指示取消,运行时可以停止工作。同样,任务本身必须检查信号,不能在中途指令被杀死。 关键思想是协作取消:运行时提供信号,代码决定如何以及何时退出。 结构化并发(Kotlin、Swift)现代语言如Kotlin(协程)和Swift(async/await)更进一步实现结构化并发:
Kotlin中的例子: 这个模式在没有不安全的抢占的情况下强制执行生命周期和取消规则。 抢占式取消(线程)其他环境,如Java或C#,通过线程提供抢占式取消:线程可以在执行中途被中断或中止。但这引入了复杂的安全问题:
JavaScript在主线程上完全避免了这一点,因为语言依赖共享内存和单线程执行。强制终止会损害稳定性和可预测性。 JavaScript的要点
JavaScript中取消的实用模式理解取消的约束是一回事,有效地应用它们是另一回事。现代JavaScript提供了工具和模式来安全、可预测地处理取消,主要围绕AbortController和协作设计。 到处传递AbortSignal一个好习惯是设计API接受AbortSignal作为一等参数: 调用者然后可以创建控制器并在需要时中止: 这个模式允许取消通过多层API调用传播,并确保在支持的地方进行资源清理。 使长时间运行的工作可中止对于CPU密集型任务或循环,你需要明确检查信号。将工作分成块并偶尔检查允许协作取消: 检查 设计取消感知的API当构建库或组件时:
例子: 这保证了可预测的取消,而不会留下部分操作或资源悬挂。 与React或Node.js结合「React」:将AbortSignal传递给fetch或useEffect中的长时间运行操作,并在清理函数中中止。 「Node.js」:许多API如fs.promises流或fetch(通过node-fetch或原生支持)接受信号。使用它们来防止服务器关闭或请求取消期间的持续资源使用。 通过一致地使用协作模式、信号和设计良好的API,你可以在JavaScript中实现健壮的取消,而不会破坏Promise、泄漏资源或创建不安全的抢占。 结论:不要再试图"杀死"PromiseJavaScript中的取消与来自其他语言的开发者期望的根本不同。Promise是对未来值的不可变占位符,而不是对运行任务的句柄。没有内置机制强制停止工作,试图以这种方式对待它们会导致脆弱、不可预测的代码。 相反,JavaScript通过AbortController和AbortSignal提供协作取消。这些工具允许代码:
关键要点是:「取消是意图,而非强制」。工作只有在执行它的代码检查信号并做出响应时才会停止。CPU密集型循环、同步计算或协作API之外的代码将继续运行,直到它们自愿退出。 通过接受这个模型:
最终,JavaScript中的取消更多地关于设计你的任务以响应和协作,而不是关于杀死Promise。理解这种区别允许开发者编写健壮、可维护的异步代码,而不会与语言执行模型对抗。 阅读原文:原文链接 该文章在 2025/12/31 10:34:42 编辑过 |
关键字查询
相关文章
正在查询... |