LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

JavaScript 取消 async/await 的真相:你大多只是在忽略结果

admin
2025年12月29日 14:45 本文热度 395

摘要

每位JavaScript开发者都会问同样的问题:为什么我不能直接取消这个异步操作?

当用户离开、组件卸载、新的请求取代旧的请求时——肯定有办法停止不再需要的工作吧?

在实践中,我们采用熟悉的模式:用Promise.race()实现超时,当结果最终到达时忽略它,或者配置AbortController然后认为问题已经解决。这种方法通常看起来有效,直到应用程序开始泄漏资源、执行迟来的副作用,或者在负载下表现不一致。

根本的、核心问题是:「JavaScript不提供任务取消作为原语」。一旦异步工作被调度,就没有强制停止它的通用机制。Promise、回调和异步函数代表的是结果和延续,而不是底层执行的所有权。

这造成了意图与现实之间的不匹配:开发者思考的是"停止工作",但语言的操作方式是让工作运行至完成,并可选择地对其结果做出反应。因此,许多所谓的取消技术只是停止等待结果,而不是停止工作本身。

理解这一差距至关重要,因为它解释了JavaScript的许多异步行为:为什么Promise不能被取消,为什么超时不会停止执行,以及为什么AbortController被设计为信令机制而非终止开关。一旦这个模型清晰,围绕取消的限制就不再感觉是偶然的——它们直接来源于JavaScript如何执行代码。


取消 vs 超时 vs 失败

JavaScript中取消经常被误解的原因之一是,它与两个截然不同的概念混淆在一起:超时和失败。这三者都可能导致"这个操作没有产生值",但它们描述的是根本不同的情况。

取消:"我不再需要这个"

取消是一个外部决定。操作本身可能完全健康且能够完成,但外部的某些东西——用户输入、应用程序状态、导航或新的请求——使得结果变得不重要。

重要的是,取消不涉及正确性。操作没有失败。只是被要求停止,因为它的结果不再需要。

在设计良好的系统中,取消是预期的和常规的,而非例外的。

超时:"我停止等待了"

超时不会取消工作。它只是限制调用者愿意等待结果的时间。

在JavaScript中,超时通常使用Promise.race()实现:

await Promise.race([
  doWork(),
  timeout(1000)
]);

当超时赢得竞赛时,等待代码恢复执行,但doWork()继续运行。它执行的任何副作用仍然会发生。它持有的任何资源将保持分配状态,直到它完成或自行清理。

如今,大多数现代API接受AbortSignal。这改善了资源清理和意图信号传递,但它没有改变根本模型:中止仍然是协作式的,只影响选择加入的代码。

这种区别很容易被忽视,因为调用者重新获得控制权,造成工作已经停止的错觉。实际上,超时只是停止了观察结果。

失败:"出错了"

失败描述内部问题:网络错误、无效输入、逻辑错误、不可用资源。它们通常表示为拒绝的Promise或抛出的错误。

与取消不同,失败不是故意的。它们表示操作即使在仍然需要其结果的情况下也无法成功完成。

将取消视为失败通常会导致笨拙的错误处理。代码开始捕获"错误",这些根本不是错误,或者抑制失败,因为它们可能只是取消。随着时间的推移,真正的失败变得难以从正常的控制流中区分出来。


为什么这个区别很重要

在JavaScript API中,超时和失败经常被重载以代替取消。这在表面上可行,但它掩盖了意图,并将责任推给调用者去猜测实际发生了什么。

一旦你分离了这些概念,一个模式就出现了:JavaScript擅长表达等待和失败,但它没有停止工作的内置概念。所有看起来像取消的东西要么是超时、被忽略的结果,或者是顶层协作协议。


为什么Promise不能被取消

当开发者问为什么在JavaScript中取消如此困难时,他们通常的意思是:为什么我不能取消Promise?毕竟,Promise是async/await的基础,大多数异步工作都是用它们表达的。如果Promise代表"任务",取消似乎应该很简单。

但Promise从未被设计用来建模任务。

Promise代表结果,而非执行

Promise是未来可用值的占位符。它没有说明该值是如何产生的,甚至没有说明是否有与之相关的正在进行的工作。当你获得一个Promise时,底层操作可能已经完成、正在进行中,或与其他消费者共享。

这种区别很微妙但很关键:Promise不拥有导致它的工作。

一旦创建,Promise必须最终解决——要么 fulfilled,要么 rejected。没有"放弃"或"取消"的第三态,因为这会破坏Promise做出的核心保证:如果你有一个对它的引用,你可以可靠地附加处理器并最终观察到一个结果。

"取消Promise"的谬论

想象一下Promise上假设的.cancel()方法。它实际上会做什么?

考虑这个:

const p = fetchData();
p.then(render);
p.then(cacheResult);

如果一个消费者调用p.cancel(),其他消费者会发生什么?它们的处理器应该停止运行吗?Promise应该拒绝吗?用什么错误?如果第三个消费者在取消后附加了.then()呢?

这些问题在没有引入全局副作用的情况下没有一致的答案。Promise是有意可共享和可组合的,取消会使它们的行为取决于谁还在观察它们。

这就是为什么取消不适合作为Promise本身的方法。取消是关于控制工作,而Promise是关于观察结果。

如果Promise可取消,什么会崩溃

使Promise可取消会波及整个异步生态系统:

  • 共享Promise会变得脆弱,因为任何消费者都可能影响其他消费者。
  • 记忆化和缓存会变得不安全——缓存的Promise可能被意外取消。
  • async/await会失去其简单的心理模型,因为等待一个Promise不再保证最终完成。

换句话说,取消会在原本独立的代码片段之间引入隐藏的耦合。

为什么取消必须存在于别处

早期的库实验过可取消的Promise,这个想法甚至出现在早期标准化讨论中。结论是一致的:取消不是Promise的属性,而是调用者和被调用者之间的协议。

该协议需要一个单独的通道:可以传递、观察和采取行动的东西——而不破坏Promise本身的语义。这就是为什么现代JavaScript将取消建模为信号,而不是对Promise的操作。

一旦你将Promise视为对未来值的不可变视图,而不是对运行任务的句柄,它们缺乏取消就不再看起来像遗漏。这是一个保持异步代码可预测和可组合的边界。


AbortController到底是什么

如果Promise不能被取消,我们如何在JavaScript中实际停止或控制异步工作?这就是理解AbortController真正做什么的关键——以及它不能做什么——对于设计取消感知代码至关重要。

AbortController作为信令机制

AbortController本质上是一个信使。它允许一段代码通知其他代码任务应该不再继续。它通过AbortSignal这样做:

const controller = new AbortController();
const signal = controller.signal;

fetch(url, { signal })
  .then(response =>console.log('获取到了!', response))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('获取被中止');
    } else {
      console.error(err);
    }
  });

// 之后,触发中止
controller.abort();

在这里,controller.abort()不会神奇地停止每一行JavaScript。相反,它通知任何协作的API——在这种情况下是fetch——工作不再需要。fetch通过拒绝其Promise并关闭底层网络连接来响应。这就是自动发生的全部。

AbortController能做什么

  • 「信号传递意图」:任何观察信号的消费者都可以做出反应。
  • 「启用资源清理」:像fetch或流这样的API可以关闭连接、释放句柄或停止产生数据。
  • 「传播取消」:信号可以通过多层API调用链传递,允许更高级别的代码请求终止低级操作。

本质上,AbortController提供了一个协作取消协议。消费者必须选择加入并决定如何响应。

AbortController不能做什么

  • 「停止任意JavaScript执行」:CPU密集型循环、同步函数或其他工作将继续运行直到完成,除非它们明确检查信号。
  • 「自动强制清理」:只有响应信号的代码才能释放资源或终止任务。
  • 「通用地取消Promise」:它不会神奇地取消底层Promise,它只是发出中止的意图。

按设计,Abort是协作式的

AbortController的协作性质是有意为之的:

  • 它避免了破坏共享状态或意外运行代码。
  • 它保留了JavaScript的运行至完成语义。
  • 它给API作者灵活性来决定如何响应中止信号,而不是强加一刀切的行为。

例如,考虑一个长时间运行的计算:

async function compute(signal{
  let i = 0;
  while (i < 1e9) {
    if (signal.aborted) {
      console.log('计算被中止');
      return;
    }
    i++;
  }
  return i;
}

没有明确检查signal.aborted,没有办法停止这个计算。信号不会"杀死"函数,它只是提供一种方式让函数注意到它应该提前退出。


资源清理 vs 任务终止

JavaScript取消中的一个常见误解是,认为信号通知任务中止会自动停止所有工作。实际上,停止任务和清理资源之间有一个关键区别,理解这一点对于编写健壮的异步代码至关重要。

停止工作 vs 清理

当你对AbortController调用controller.abort()时,观察信号的API通常会释放资源:

  • fetch关闭底层网络连接。
  • 流停止产生数据并可以释放缓冲区。
  • 数据库或文件句柄如果API支持中止信号,可能会被关闭。

这就是"资源清理"的含义:系统确保套接字、内存缓冲区或文件描述符不会被留下悬挂。清理对于防止内存泄漏、连接耗尽或其他细微错误至关重要。

然而,资源清理不会自动停止所有正在进行的任何工作。任何CPU密集型计算、同步逻辑或协作API之外的代码都会继续运行,直到自然完成。

为什么JavaScript专注于清理,而非终止

JavaScript的执行模型强制运行至完成:一旦函数开始,它将运行到当前同步块的结束。事件循环不允许抢占式中断。结果是:

  • 强制终止函数中途执行可能会使共享状态不一致。
  • 局部副作用(如部分更新的DOM或部分写入的文件)可能损坏系统。
  • 内存安全和可预测的执行将受到损害。

相反,JavaScript强调协作模式,其中代码自愿检查取消并干净地退出。AbortController符合这个模型:它发出意图,API或函数决定如何响应。

AbortController作为清理触发器

大多数支持AbortSignal的现代API专注于资源的干净终止:

const controller = new AbortController();
const signal = controller.signal;
const stream = someStreamAPI({ signal });

controller.abort(); // 触发清理

在这里,stream可能停止产生数据、关闭内部缓冲区并释放文件描述符。任何消费代码都可以注意到中止并停止进一步处理。工作不会被强制终止:相反,API和调用者协作安全地退出。

要停止CPU密集型任务或自定义计算,开发者必须定期检查signal.aborted,参见前面Abort is cooperative by design部分中的例子。

这种清理+协作退出的组合是JavaScript提供的取消模式。它在允许开发者回收资源并优雅地停止长时间运行的操作的同时,保留了安全性。


为什么JavaScript不能强制停止代码

JavaScript中的取消与其他语言工作方式不同的原因之一是语言执行代码的方式。理解这一点是实现AbortController不能神奇地"杀死"函数或Promise的关键。

JavaScript中没有抢占

JavaScript运行在单线程事件循环上。每个函数在执行下一个任务之前运行至完成:

function busyLoop({
  for (let i = 0; i < 1e9; i++) {
    // CPU密集型工作
  }
  console.log('完成!');
}

busyLoop();
console.log('这只在busyLoop完成后运行');

busyLoop()运行时,事件循环不能中断它。没有机制可以注入代码来强制在块中间停止执行。这个设计使JavaScript可预测,但也意味着取消必须是协作式的。

为什么强制终止不安全

想象一下如果JavaScript允许任意终止:

  • 共享的可变状态可能变得不一致:
    obj.count++;
    // 在这里终止 -> obj.count 从未正确增加
  • 局部更新可能损坏数据:
    arr.push(newItem);
    // 在这里终止 -> arr 处于不一致状态
  • Promise永远无法可靠地被观察: 期望值的消费者可能永远不会收到通知,如果底层任务在执行中途消失。

因为JavaScript鼓励共享对象和可组合的异步代码,抢占式终止本质上是不安全的。

为什么Web Workers从根本上不能改变这一点

一些开发者认为:"我可以在Web Worker中运行CPU工作并终止它。"技术上,你可以:

const worker = new Worker('worker.js');
worker.terminate(); // 杀死worker线程

但这是进程级终止,而非任务级取消:

  • terminate()停止worker中的所有代码,无论它在做什么。
  • 对worker内的单个任务或Promise没有细粒度控制。
  • 传输中的消息可能丢失,留下部分处理的数据。

Web Workers提供了一种隔离可能需要强制杀死的任务的方法,但在主线程内,JavaScript仍然无法安全地抢占代码。这就是为什么像AbortController这样的协作信号是首选模式:它们让代码在清理资源的同时自愿退出。


其他语言如何建模取消

JavaScript的协作取消模型可能感觉受限,但看看其他语言有助于解释原因。不同的环境在安全性、控制性和可组合性之间做出不同的权衡。

协作取消(Go、Rust异步)

像Go和Rust这样的语言提供显式的协作取消机制:

「Go:上下文传播」

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

select {
case <-doWork(ctx):
    fmt.Println("完成")
case <-ctx.Done():
    fmt.Println("取消")
}

ctx被显式传递给所有可能需要取消的函数。工作本身检查上下文并提前退出。资源可以以结构化的方式清理。

这在概念上类似于JS中的AbortController:一个信号沿调用链传递,需要协作。

「Rust:异步取消」

Rust中的Future可以用取消信号轮询。任务在控制点让出控制,如果信号指示取消,运行时可以停止工作。同样,任务本身必须检查信号,不能在中途指令被杀死。

关键思想是协作取消:运行时提供信号,代码决定如何以及何时退出。

结构化并发(Kotlin、Swift)

现代语言如Kotlin(协程)和Swift(async/await)更进一步实现结构化并发:

  • 任务绑定到父作用域。
  • 当父级取消时,所有子任务收到取消信号。
  • 这确保了异步工作是有界的、可预测的,并且易于清理。

Kotlin中的例子:

val job = launch {
    val child = launch {
        repeat(1000) { i ->
            println("工作中 $i")
            delay(100)
        }
    }
    delay(500)
    child.cancel() // 协作取消
}

这个模式在没有不安全的抢占的情况下强制执行生命周期和取消规则。

抢占式取消(线程)

其他环境,如Java或C#,通过线程提供抢占式取消:线程可以在执行中途被中断或中止。但这引入了复杂的安全问题:

  • 共享可变状态可能变得不一致。
  • 锁或资源可能永远不会被释放。
  • 出于安全原因,库经常劝阻强制线程终止。

JavaScript在主线程上完全避免了这一点,因为语言依赖共享内存和单线程执行。强制终止会损害稳定性和可预测性。

JavaScript的要点

  • 像AbortController这样的协作信号是Go、Rust或Kotlin中最接近取消的等价物。
  • JavaScript故意避免抢占以维护安全性和简单性。
  • JS取消中的许多"陷阱"是其他语言在选择安全性而非暴力控制时必须管理的相同权衡。

JavaScript中取消的实用模式

理解取消的约束是一回事,有效地应用它们是另一回事。现代JavaScript提供了工具和模式来安全、可预测地处理取消,主要围绕AbortController和协作设计。

到处传递AbortSignal

一个好习惯是设计API接受AbortSignal作为一等参数:

async function fetchWithSignal(url, signal{
  const response = await fetch(url, { signal });
  const data = await response.json();
  return data;
}

调用者然后可以创建控制器并在需要时中止:

const controller = new AbortController();
const signal = controller.signal;

fetchWithSignal('/api/data', signal)
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError'console.log('请求已取消');
    else console.error(err);
  });

// 之后
controller.abort();

这个模式允许取消通过多层API调用传播,并确保在支持的地方进行资源清理。

使长时间运行的工作可中止

对于CPU密集型任务或循环,你需要明确检查信号。将工作分成块并偶尔检查允许协作取消:

async function heavyComputation(signal{
let result = 0;
for (let i = 0; i < 1e9; i++) {
    if (signal.aborted) {
      console.log('计算被中止');
      return;
    }
    result += i;
    if (i % 1e6 === 0awaitPromise.resolve(); // 让出事件循环
  }
return result;
}

检查signal.aborted让函数提前退出。偶尔让出防止阻塞事件循环太长时间。这个方法反映了其他语言中的结构化并发:任务与取消协作并保持响应。

设计取消感知的API

当构建库或组件时:

  • 接受AbortSignal而不是发明自定义取消标志。
  • 记录取消做什么:
    • 它是否停止网络请求?
    • 它是否释放内存或文件句柄?
    • 它是否停止计算?
  • 避免隐藏的后台工作:
    • 确保取消的任务不会继续修改共享状态。
  • 通过所有依赖操作传播信号:
    • 如果高级操作被中止,所有子操作应该观察到相同的信号。

例子:

async function processBatch(batch, signal{
  const results = [];
  for (const item of batch) {
    if (signal.aborted) break;
    results.push(await processItem(item, signal));
  }
  return results;
}

这保证了可预测的取消,而不会留下部分操作或资源悬挂。

与React或Node.js结合

「React」:将AbortSignal传递给fetch或useEffect中的长时间运行操作,并在清理函数中中止。

「Node.js」:许多API如fs.promises流或fetch(通过node-fetch或原生支持)接受信号。使用它们来防止服务器关闭或请求取消期间的持续资源使用。

通过一致地使用协作模式、信号和设计良好的API,你可以在JavaScript中实现健壮的取消,而不会破坏Promise、泄漏资源或创建不安全的抢占。


结论:不要再试图"杀死"Promise

JavaScript中的取消与来自其他语言的开发者期望的根本不同。Promise是对未来值的不可变占位符,而不是对运行任务的句柄。没有内置机制强制停止工作,试图以这种方式对待它们会导致脆弱、不可预测的代码。

相反,JavaScript通过AbortController和AbortSignal提供协作取消。这些工具允许代码:

  • 信号表明工作不再需要
  • 清理资源如网络连接、流或文件句柄
  • 使任务能够提前退出,如果它们选择加入

关键要点是:「取消是意图,而非强制」。工作只有在执行它的代码检查信号并做出响应时才会停止。CPU密集型循环、同步计算或协作API之外的代码将继续运行,直到它们自愿退出。

通过接受这个模型:

  • API变得更可预测和可组合
  • 资源泄漏和副作用被最小化
  • 异步代码可以干净地处理用户驱动的中断

最终,JavaScript中的取消更多地关于设计你的任务以响应和协作,而不是关于杀死Promise。理解这种区别允许开发者编写健壮、可维护的异步代码,而不会与语言执行模型对抗。


阅读原文:原文链接


该文章在 2025/12/31 10:34:42 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved