给 DOM 元素注册事件,最常见的是 onclick 和 addEventListener。两种方式都能完成点击、输入等交互,但在覆盖、解绑和多个处理函数这些细节上差别很大。这里把我自己常用、也最容易忘的点记下来。
一、两种原生绑定方式
传统方式是给事件属性赋值,也可以写成 HTML 行内事件。行内事件会把结构和行为混在一起,我一般不会使用;属性赋值在小脚本里能用,但一个属性只能保存一个处理函数。
const button = document.querySelector('#save');
button.onclick = function (event) {
console.log('保存', event.type);
};
// 后一次赋值会覆盖前一个处理函数
button.onclick = function () {
console.log('新的处理函数');
};
// 解绑
button.onclick = null;
更常用的是 addEventListener。它可以为同一个元素、同一种事件注册多个处理函数,并且能指定捕获阶段、只执行一次等选项。
function saveDraft(event) {
console.log('保存草稿', event.currentTarget);
}
function logClick() {
console.log('记录点击');
}
button.addEventListener('click', saveDraft);
button.addEventListener('click', logClick);
button.addEventListener('click', () => console.log('只执行一次'), {
once: true,
});
二、解绑时必须拿到同一个函数引用
removeEventListener 不是根据函数内容删除,而是根据同一个函数对象删除。因此匿名函数很方便,却不适合需要主动解绑的场景。
function handleClick() {
console.log('click');
}
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick); // 可以解绑
button.addEventListener('click', () => console.log('click'));
button.removeEventListener('click', () => console.log('click')); // 无法解绑
如果绑定时传了捕获阶段,解绑时也要保持一致。比如使用 { capture: true } 注册,就不能按默认冒泡阶段去移除。组件销毁、弹窗关闭或重复初始化时,及时解绑能避免重复响应和不必要的引用保留。
三、事件对象里优先分清 target 和 currentTarget
事件回调会收到 event。其中 event.target 是实际触发事件的最深层元素;event.currentTarget 是当前正在执行监听器的元素。点击按钮内的图标时,两者可能不同。
list.addEventListener('click', function (event) {
console.log(event.target); // 可能是 li 内的 span 或图标
console.log(event.currentTarget); // 一直是 list
console.log(this === event.currentTarget); // 普通函数中为 true
});
上面的 this 结论只适用于普通函数。箭头函数没有自己的 this,会沿用外层作用域,所以需要当前绑定元素时,直接使用 event.currentTarget 更明确。
四、列表类交互可以考虑事件委托
很多列表项都有相同行为时,不一定要给每一项单独绑定事件。可以把监听器绑定在稳定存在的父元素上,再从 event.target 向上找到真正要处理的子元素。这种方式对后来插入的列表项也有效。
list.addEventListener('click', (event) => {
const item = event.target.closest('[data-id]');
if (!item || !list.contains(item)) return;
openDetail(item.dataset.id);
});
我自己的默认选择是:需要一个简单处理函数时也优先 addEventListener;需要解绑就保存函数引用;处理动态列表时再考虑事件委托。这样比依赖 onclick 更不容易在后续维护中踩到覆盖和重复绑定的问题。