如何移除事件侦听器?

大家好,这里是大家的林语冰。本期《前端翻译计划》共享的是复盘在 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()

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

相关推荐
Avan_菜菜23 分钟前
AI 能写代码了,为什么我反而开始要求它先写文档?
前端·github·ai编程
JieE2124 小时前
LeetCode 226. 翻转二叉树|JS 递归超详细拆解,二叉树入门经典题
javascript·算法
JieE2124 小时前
LeetCode 104. 二叉树的最大深度|递归思路超详细拆解
javascript·算法
爱勇宝5 小时前
鸿蒙生态的下半场:开发者不只要能开发,还要能赚钱
android·前端·程序员
IT_陈寒8 小时前
SpringBoot这个自动配置坑我跳了三次
前端·人工智能·后端
kyriewen8 小时前
我用 AI 一周写完了整个项目,上线第一天就崩了——这是我踩过最贵的 5 个坑
前端·javascript·ai编程
Larcher8 小时前
AI Loop:让AI像人一样自主完成任务的核心机制
javascript·人工智能·设计模式
默_笙8 小时前
🃏 JS 只有 8 种数据类型,但我花了 2 天才搞懂 null 和 undefined 的区别
javascript
牧艺8 小时前
从零到协同:构建类飞书在线文档系统的五个技术重难点
前端·人工智能
jump_jump9 小时前
流式 HTML:从 htmx 片段装配到浏览器原生增量渲染
javascript·性能优化·前端工程化