大家好,这里是大家的林语冰。本期《前端翻译计划》共享的是复盘在 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 个参数:
- 要移除的侦听器类型
- 该侦听器的回调函数
- 一个选项对象
但此处(可能)存在棘手的部分:这些确切的参数必须与设置侦听器时使用的参数完全匹配,包括对内存中回调的相同引用。否则,.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()
。
您现在收看的是《前端翻译计划》,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎持续关注前端生态。谢谢大家的点赞,掰掰~
