在运行时清理代码是构建高效、可预测应用程序的一个不可谈判的部分。JavaScript中的一种方法是很好地管理事件侦听器,特别是在不再需要它们时删除它们。
有几种方法可以做到这一点,每种方法都有自己的权衡,使其在某些情况下更为合适。我们将介绍一些最常用的策略,以及当您在任何给定时间尝试决定哪种策略最适合工作时需要记住的一些考虑因素。
我们使用一个带有点击事件的按钮来测试:
<button id="button">Do Something</button>
<script>
document.getElementById('button').addEventListener('click', () => {
console.log('clicked!');
});
</script>
使用 Chrome’s getEventListeners() function, 你可以看到在这个element上只存在一个listener:
在你需要删除该监听器时, 下面是你可以用到的几种方案.
使用 .removeEventListener()
这应该是最常用的, 但是对你的精神最具威胁(译者:?). .removeEventListener() 方法接收三个参数: 需要移除的事件类型, 监听器的回调函数, 和一个options对象.
但这里有(潜在的)棘手的部分: 这些参数必须与创建监听时的传入参数完全匹配, 包括回调函数对需要传入相同的指针(只想相同的地址). 否则.removeEventListener()将会失效.
因为上述原因,这样的代码将会没有效果:
document.getElementById('button').addEventListener('click', () => {
console.log('clicked!');
});
document.getElementById('button').removeEventListener('click', () => {
console.log('clicked!');
});
尽管具有特殊性,但.removeEventListener()的优点是其目的非常明确。当你通读代码时,它在做什么是毫无疑问的。
使用 .addEventListener()’的 once 配置项
.addEventListener() 方法在只触发一次的情况下提供了一种自我清除的配置项: once . 顾名思义. 如果这个配置项被设置成true, 在这个监听器触发一次后就会被清除:
const button = document.getElementById('button');
button.addEventListener('click', () => {
console.log('clicked!');
}, { once: true });
// 'clicked!'
button.click();
// No more listeners!
getEventListeners(button) // {}
假设它适合您的用例,如果您热衷于使用匿名函数,那么这种方法可能是合适的,因为您的监听器只需要调用一次。
复制或者替代Node节点
有时,你不知道node节点上有多少监听器,但是想一次性处理, 这种情况下, 可以复制这个节点并且直接替换原有节点. 使用 .cloneNode() method, 通过.addEventListener()创建的侦听器都不会被复制,从而使其成为一个干净的Node节点
回到客户端JavaScript的石器时代,您可以通过查询父节点并用克隆替换特定的子节点来实现这一点:
button.parentNode.replaceChild(button.cloneNode(true), button);
但是在现在,可以使用.replaceWith()进行简化:
button.replaceWith(button.cloneNode(true));
有一件事可能会让你感到困惑,那就是保留了内部监听器,这意味向下面代码这样带有onclick属性的按钮仍将按定义启动:
<button id="button" onclick="console.log('clicked!')">
Do Something
</button>
总之,如果你需要用暴力无差别地删除任何类型的听众,这是一个值得尝试的选择。然而,在不利方面,它的目的不那么明显(语义化)。有些人甚至称之为黑科技。
使用 AbortController()
这个对于我来说比较新. 我从 this tweet 来自 Caleb Porzio 中学习得到. 如果你像我一样,也只听过 AbortController 被用于 cancel fetch() requests. 但它显然比这更灵活.
“.addEventListener()”可以配置一个“signal”,用于强制中止/删除侦听器。当相应的控制器调用“.abort()”时,该信号将触发侦听器被删除:
const button = document.getElementById('button');
const controller = new AbortController();
const { signal } = controller;
button.addEventListener('click', () => console.log('clicked!'), { signal });
// Remove the listener!
controller.abort();
最明显的优点可能是更具工程结构。在我看来,这是一种更清晰的方法,可以在不调用.removeEventListener()的情况下删除一个侦听器。但还有一个更具战术性的优势:您可以使用一个signal的同时删除多个侦听器,无论是哪种类型。使用匿名函数也完全可以:
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 });
// Remove all listeners at once:
controller.abort();
我遇到的犹豫的唯一原因是浏览器支持。这是一个相对较新的功能,自2021(v90)以来,Chrome才完全支持它。因此,如果您需要支持超过几年的浏览器版本,请记住这一点。
如何选择remove方案?
下面是我如何在一瞬间选择一个而不是另一个:
- 使用
.removeEventListener()当他的回调函数使用变量传入 并且 在创建后可以很容易的获取到. - 使用
.addEventListener()的once配置项 当你只需要调用一次监听器. - 使用
clone & replace当你需要一次性去除所有监听. - 使用
AbortController()如果您有一系列相同的侦听器需要同时删除,或者您只是喜欢这个语法.
可能遗漏的内容
除了这些,我很有可能错过了另一个选择。如果你碰巧有一个,请随时发表评论或通过其他方式让我知道。至少,我希望这有助于在头脑中组织一些清理事件侦听器的可用路径,也有助于为下次需要在代码中管理它们时做好准备.