如何移除事件侦听器?

大家好,这里是大家的林语冰。本期《前端翻译计划》共享的是复盘在 JS 中移除事件侦听器的若干最常见方法。

免责声明

本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 You've Got Options for Removing Event Listeners

在运行时清理代码是构建高效、可预测的 App 不可或缺的一部分。JS 中实现的方法之一是妥善地管理事件侦听器 ------ 具体而言,就是在不需要它们时将其移除。

有一大坨方法可以实现,每种方法都有自己因地制宜的权衡。我们将介绍若干最常用的策略,以及当您在任何特定时机试图决定最佳策略时,需要牢记的某些注意事项。

我们将修改下述设置 ------ 一个附加了单个 click 事件侦听器的按钮:

html 复制代码
<button id="button">Do Something</button>

<script>
  document.getElementById('button').addEventListener('click', () => {
    console.log('clicked!')
  })
</script>

使用 Chrome 的 getEventListeners() 函数,您能且仅能看到一个附加到该元素的侦听器:

如果您需要移除该侦听器,您可以使用下述方法。

使用 .removeEventListener()

这可能是最显而易见、但也最有可能让你头皮发麻的方案。.removeEventListener() 方法接受 3 个参数:

  1. 要移除的侦听器类型
  2. 该侦听器的回调函数
  3. 一个选项对象

但此处(可能)存在棘手的部分:这些确切的参数必须与设置侦听器时使用的参数完全匹配,包括对内存中回调的相同引用。否则,.removeEventListener() 不会执行任何操作。

考虑到这一点,下述代码并无卵用:

js 复制代码
document.getElementById('button').addEventListener('click', () => {
  console.log('clicked!')
})

document.getElementById('button').removeEventListener('click', () => {
  console.log('clicked!')
})

尽管该回调目测与最初附加的回调相同,但它并不是相同的引用。解决方案是将回调设置为变量,并在 .addEventListener().removeEventListener() 中引用它。

js 复制代码
const myCallback = () => {
  console.log('clicked!')
}

document.getElementById('button').addEventListener('click', myCallback)
document.getElementById('button').removeEventListener('click', myCallback)

或者,对于特定用例,您还可以通过从函数本身引用伪匿名(pseudo-anonymous)函数来移除侦听器:

js 复制代码
document
  .getElementById('button')
  .addEventListener('click', function myCallback() {
    console.log('clicked!')

    this.removeEventListener('click', myCallback)
  })

尽管存在特殊性,.removeEventListener() 的优点是其目的不言而喻。当您阅读代码时,您对其功能心照不宣。

使用 .addEventListener()once 选项

.addEventListener() 方法附带了一个工具:once 选项,如果是一次性使用,就可以用它自我清理。如果 once 设置为 true,侦听器会在首次调用后自动删除自身:

js 复制代码
const button = document.getElementById('button')

button.addEventListener(
  'click',
  () => {
    console.log('clicked!')
  },
  { once: true }
)

// 'clicked!'
button.click()

// 侦听器已移除!
getEventListeners(button) // {}

假设它适合您的用例,如果您热衷于使用匿名函数,那么此方法可能恰到好处,因为您的侦听器只需调用一次。

克隆/替换节点

有时,您不知道给定节点上所有激活的侦听器,但您明确想要消灭它们。在这种情况下,可以克隆整个节点并用该克隆替换自身。使用 .cloneNode() 方法,通过 .addEventListener() 连接的侦听器都不会被保留。

回到客户端 JS 的石器时代,您会看到通过查询父节点,并用克隆替换特定子节点来完成此操作:

js 复制代码
button.parentNode.replaceChild(button.cloneNode(true), button)

但在现代浏览器中,可以使用 .replaceWith() 简化:

js 复制代码
button.replaceWith(button.cloneNode(true))

可能会让您进退维谷的一件事是保留了内部侦听器,这意味着,具有 onclick 属性的按钮仍会按定义触发:

html 复制代码
<button id="button" onclick="console.log('clicked!')">Do Something</button>

总而言之,如果您需要用无差别地暴力移除任意类型的侦听器,那么这值得一试。虽然但是,它的缺点是其目的不太直观。有些人甚至可能称其为奇技淫巧。

使用 AbortController()

此方案于我而言乃知识盲区。如果您像我一样,您可能只听说过 AbortController 可用于取消 fetch() 请求。但它显然比这更灵活。

截至最近,.addEventListener() 可以配置有 signal 来强制中止/移除侦听器。当相应的控制器调用 .abort() 时,该 signal 会触发移除侦听器:

js 复制代码
const button = document.getElementById('button')
const controller = new AbortController()
const { signal } = controller

button.addEventListener('click', () => console.log('clicked!'), { signal })

// 移除侦听器!
controller.abort()

最明显的优势可能是人体工程学。(在我看来)这是一种更清晰的移除侦听器的方法,而不会出现 .removeEventListener() 的潜在问题。但还有一个更具战略意义的优势:您可以使用一个 signal 一次性移除任意类型的多个监听器。使用匿名函数也问题不大:

js 复制代码
const button = document.getElementById('button')
const controller = new AbortController()
const { signal } = controller

button.addEventListener('click', () => console.log('clicked!'), { signal })
window.addEventListener('resize', () => console.log('resized!'), { signal })
document.addEventListener('keyup', () => console.log('pressed!'), { signal })

// 一次性移除所有侦听器:
controller.abort()

我唯一犹豫不决的原因是浏览器的支持。这是一项相对较新的功能,自 2021 年(v90)起 Chrome 才提供全面支持。因此,如果您需要支持几年前的浏览器版本,请牢记这一点。

我该如何选择?

总而言之,实事求是即可:

  • 如果回调函数已赋值给变量,且可以轻松访问添加侦听器的位置,请使用 .removeEventListener()
  • 如果您只需触发一次回调,请使用 .addEventListener() 中的 once 选项。
  • 如果您需要无差别地消灭多个侦听器,请使用克隆和替换方法。
  • 如果您想立即强制移除一系列侦听器,或者您只是喜欢该语法,请使用 AbortController()

您现在收看的是《前端翻译计划》,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎持续关注前端生态。谢谢大家的点赞,掰掰~

相关推荐
聪明的墨菲特i9 分钟前
React与Vue:哪个框架更适合入门?
开发语言·前端·javascript·vue.js·react.js
时光少年10 分钟前
Android 副屏录制方案
android·前端
拉不动的猪17 分钟前
v2升级v3需要兼顾的几个方面
前端·javascript·面试
时光少年19 分钟前
Android 局域网NIO案例实践
android·前端
半兽先生35 分钟前
VueDOMPurifyHTML 防止 XSS(跨站脚本攻击) 风险
前端·xss
冴羽38 分钟前
SvelteKit 最新中文文档教程(20)—— 最佳实践之性能
前端·javascript·svelte
Nuyoah.39 分钟前
《Vue3学习手记2》
javascript·vue.js·学习
Jackson__44 分钟前
面试官:谈一下在 ts 中你对 any 和 unknow 的理解
前端·typescript
zpjing~.~1 小时前
css 二维码始终显示在按钮的正下方,并且根据不同的屏幕分辨率自动调整位置
前端·javascript·html
红虾程序员1 小时前
Linux进阶命令
linux·服务器·前端