Promise.withResolvers 怎么把事件回调封装成可取消等待
用 Promise.withResolvers() 取得 promise、resolve 和 reject,把目标事件回调接到 resolve,把 AbortSignal 的 abort 回调接到 reject;两条路径结束时都移除监听器。还要在注册前检查 signal.aborted,否则拿一个已经取消的信号开始等待,可能一直悬挂。
它不是“可取消 Promise”语法:Promise 只承载结果,取消语义由适配函数实现。用户取消的是本次订阅和等待,不是事件源自身。以页面上的“确认继续”和“取消等待”为例,取消后按钮仍然可以发出点击事件,但不应再唤醒这轮等待。
把事件桥接和取消请求分开
这种小型适配可以叫作一次性事件桥接。调用方希望用 await 描述“等一次确认再继续”,浏览器却通过监听器交付点击;两者之间缺的是结果结算和资源释放的共同约定。
withResolvers 返回三个成员,方便同一作用域的不同回调共享结算能力。我们只向调用方返回 promise,把 resolve、reject 留在适配器内部。否则业务代码也能任意结算这轮等待,清理规则会变得难以保证。它没有增加 Promise 的状态种类,也没有自动识别 AbortSignal。
| 对象 | 职责 | 不能代替的工作 |
|---|---|---|
| EventTarget | 提供目标事件 | 不会知道调用方是否仍在等待 |
| Promise | 交付一次成功或拒绝 | 不会自动解绑监听器 |
| AbortSignal | 表达取消状态与原因 | 不会自动拒绝任意 Promise |
| cleanup | 移除本轮监听器 | 不负责停止事件源和其他订阅 |

用同一个结束入口清理两类监听器
以下实现面向浏览器原生 EventTarget、字符串事件类型和可选的原生 AbortSignal。它等待注册之后交付到该监听器的第一个事件,不回放历史事件,也不判断事件的业务内容。
function waitForEvent(target, type, { signal } = {}) {
// 仅桥接原生 EventTarget;返回一次事件,而不是持续订阅
const { promise, resolve, reject } = Promise.withResolvers();
let finished = false;
function cleanup() {
// 添加和移除均使用 capture=false,回调引用必须相同
target.removeEventListener(type, onEvent, false);
signal?.removeEventListener("abort", onAbort, false);
}
function finish(ok, value) {
// 一次结束只清理一次,避免后来的回调改变业务处理
if (finished) return;
finished = true;
cleanup();
if (ok) resolve(value);
else reject(value);
}
function onEvent(event) {
finish(true, event);
}
function onAbort() {
// 原样保留取消原因,调用方不必只依赖 AbortError 名称
finish(false, signal.reason);
}
if (signal?.aborted) {
reject(signal.reason);
return promise;
}
try {
target.addEventListener(type, onEvent, { once: true, capture: false });
signal?.addEventListener("abort", onAbort, { once: true, capture: false });
// 补查状态;已经取消的 signal 不会为新监听器重放 abort
if (signal?.aborted) onAbort();
} catch (error) {
// 注册异常也走统一清理,不遗留已添加的监听器
finish(false, error);
}
return promise;
}
finished 是本轮业务结束标记,不是读取 Promise 状态的方法。原生 Promise 自身已经防止重复结算;这里的额外标记让资源清理和业务结束共用一个入口。cleanup 在 resolve 或 reject 前执行,因此成功分支不会把 abort 回调留到以后,取消分支也不会留下目标事件回调。
预先取消直接以 signal.reason 拒绝,而且还没添加监听器。注册之后再检查一次状态是防御性处理,不意味着原生同步注册过程中存在任意异步线程插入;重点是不能只监听未来 abort 而完全忽略已有取消状态。本文不覆盖重写了监听器方法、带特殊副作用的非原生事件对象。
这里特意显式移除监听器,而不是单纯给 addEventListener 传入 signal。后者可以在取消时移除事件监听器,但仍然需要让 promise 拒绝;成功时也需要解除我们自己注册的 abort 监听器。清理应由拥有本轮订阅的适配器负责。

在确认按钮与取消按钮之间等待一次
把前面的 waitForEvent 定义与下面代码放入同一个页面。按钮不是生成的界面图片,也不是运行证据;这是读者可自行试用的最小标记与脚本。
async function runConfirmation() {
const confirm = document.getElementById("confirm");
const cancel = document.getElementById("cancel");
const status = document.getElementById("status");
const controller = new AbortController();
const cancelled = new DOMException("用户取消等待", "AbortError");
function onCancel() {
// 取消本次等待,不禁用按钮,也不停止整个页面
controller.abort(cancelled);
}
cancel.addEventListener("click", onCancel, { once: true });
status.textContent = "正在等待确认";
try {
await waitForEvent(confirm, "click", { signal: controller.signal });
status.textContent = "已确认继续";
} catch (error) {
// 使用本次取消原因区分主动取消与其他异常
if (error === cancelled) status.textContent = "已取消等待";
else {
status.textContent = "等待失败";
console.error(error);
}
} finally {
// 成功时取消按钮没有被点击,once 不会替我们移除它
cancel.removeEventListener("click", onCancel);
}
}
// 页面只启动这一轮;每轮等待都需要新的 controller
void runConfirmation();
先点确认,状态文字应进入确认分支;重新载入页面后先点取消,应进入取消分支。这里的“应”描述示例预期,不是跨浏览器测试记录。调用方通过 catch 及时接住拒绝,也把取消按钮自己的监听器放入 finally 清理:适配器只管理它创建的监听器,不会替调用方管理整个页面。
若组件卸载或用户关闭对话框,同样应让本轮 controller.abort() 被调用。每次新等待创建新的 controller;已经 aborted 的信号不能恢复成未取消状态。未提供信号而且目标事件永远不发生时,等待会保持未结束,因此组件生命周期中不要省略取消入口。
只用 once 或 Promise.race 为什么不够
once 只在对应事件实际交付后移除那个监听器。确认成功时,abort 可能永远不会发生;取消发生时,目标点击也可能永远不会发生。它们各自的 once 都不会清理另一条路径,正是显式 cleanup 必须处理两边的原因。
Promise.race 可以选出较早的结果,但不会取消输掉的等待。如果事件 Promise 只是永久挂着一个监听器,即使外层 race 已经因取消返回,这个监听器仍可能保留闭包和引用。把“外层 await 已结束”当成“底层订阅已释放”,才是最常见的漏项。
移除时必须保留相同的事件类型、回调引用和 capture 值。重新写一个同内容箭头函数是新的函数对象,无法匹配旧监听器。本文把 capture 固定为 false,不把复杂的监听选项透传给调用方,减少清理约定的分歧。
如果事件回调更早进入 finish,成功结果不会被后续 abort 反转;反过来也一样。判断依据是适配器先处理了哪条结束路径,而不是两个输入在用户感觉上哪个更早。取消已成功结束的等待,并不能撤回已经开始的后续业务动作。
什么时候不该把事件变成一次等待
确认、一次准备完成通知适合这种桥接。持续的鼠标移动、实时消息订阅并不适合塞进同一个 promise:它只交付一次结果,后续事件应使用持续监听或其他明确的订阅接口。本文也不包含失败事件转换;如果事件源另有 error 事件,必须额外设计失败分支和它的清理,而不能假设这里会自动捕获。
采用前可检查:事件目标是否是约定的 EventTarget;调用是否先于需要等待的事件;取消后只需解除等待还是还需停止底层操作;是否及时接住拒绝;组件退出时是否发送取消;正常成功后是否释放 abort 监听器。把这些职责写清,再选择是否采用 withResolvers。
旧浏览器没有 withResolvers 怎么办?
使用局部能力函数,替换 waitForEvent 开头获取三个成员的一行即可。以下适配面向原生 Promise,不是支持任意 Promise 子类的完整标准 polyfill,也不意味着旧环境同时支持 AbortSignal 的所有属性。
function createResolvers() {
// 局部兼容适配,不修改全局 Promise
if (typeof Promise.withResolvers === "function") {
return Promise.withResolvers();
}
let resolve, reject;
const promise = new Promise((accept, fail) => {
// 原生 Promise 的 executor 同步执行,两个引用在返回前已赋值
resolve = accept;
reject = fail;
});
return { promise, resolve, reject };
}
替换后调用 createResolvers() 获取对象;仍需按实际部署环境检查 AbortSignal.reason 等所用接口。语法转译也不等于自动补上运行时静态方法。
取消为什么不一定得到 AbortError?
AbortController.abort 可以携带自定义原因,signal.reason 不必是 Error。本文原样传递它;示例用本次创建的取消对象比较,不把所有异常都误判为用户取消。
只需要等一个事件,没有取消入口也能用吗?
可以不传 signal,但生命周期中仍要确保事件最终可达。如果页面或组件可能离开,缺少取消入口会让挂起等待难以回收。withResolvers 的简洁不能代替资源所有权设计。
文档与规范
Promise 能力说明:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers
取消接口约定:https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
监听器移除规则:https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
ECMAScript 规范:https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-promise.withresolvers
农产品加工企业建立批次追溯记录要记录哪些字段
- 上一篇
- 农产品加工企业建立批次追溯记录要记录哪些字段
- 下一篇
- 冷链食品运输交接时怎样整理温度记录
-
- 文章 · 前端 | 1小时前 | 前端 · javascript · SyntaxError Web Components customElements.whenDefined Custom Elements 自定义元素名称
- customElements.whenDefined 传入无效标签名为什么直接报错
- 223浏览 收藏
-
- 文章 · 前端 | 1小时前 |
- customElements.whenDefined 怎么等待一组组件升级后再移除占位
- 222浏览 收藏
-
- 文章 · 前端 | 1小时前 |
- Declarative Shadow DOM 和客户端 attachShadow 怎么选择首屏渲染方案
- 440浏览 收藏
-
- 文章 · 前端 | 1小时前 | 前端 · 可访问性 · Template Shadow DOM shadowrootmode delegatesFocus
- template shadowrootmode 怎么在闭合阴影根中保留焦点委托
- 120浏览 收藏
-
- 文章 · 前端 | 1小时前 | 前端 · SSR Web Components CustomElementRegistry Declarative Shadow DOM
- Declarative Shadow DOM SSR 输出后为什么自定义元素没有升级
- 387浏览 收藏
-
- 文章 · 前端 | 1小时前 |
- CSS white-space-collapse 怎么在富文本预览中保留换行并压缩空格
- 308浏览 收藏
-
- 文章 · 前端 | 1小时前 |
- CSS white-space-collapse 的 preserve-spaces 为什么让行宽计算变了
- 222浏览 收藏
-
- 文章 · 前端 | 1小时前 |
- CSS mask-composite 在 Safari 和 Firefox 结果不同怎么办
- 103浏览 收藏
-
- 文章 · 前端 | 1小时前 |
- CSS mask-composite 怎么叠加两层遮罩做镂空边框
- 253浏览 收藏
-
- 文章 · 前端 | 1小时前 |
- CSS :has 不支持时怎么保留可用的降级样式
- 158浏览 收藏
-
- 文章 · 前端 | 1小时前 | css · CSS选择器优先级 关系选择器 CSS :is CSS :has CSS :where
- CSS :has 和 :is() 组合后为什么样式优先级变了
- 474浏览 收藏
-
- 文章 · 前端 | 1小时前 |
- CSS :has 怎么只根据直接子元素状态切换卡片样式
- 111浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 456次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 521次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 536次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 484次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 304次使用
-
- Go语言对前端领域的入侵WebAssembly运行原理
- 2022-12-31 130浏览
-
- go+gin 静态资源路由与后端api路由冲突如何解决?
- 2023-02-24 414浏览
-
- 分片上传文件,后端接收怎么生成了一个文件名为blob的文件?
- 2023-01-09 163浏览
-
- 除了cookie之外,还能用什么方法做验证码功能?
- 2023-01-22 137浏览
-
- 使用指南怎么生成
- 2023-01-18 160浏览

