“代理”可以理解为代劳或者帮忙,或者类似你的事就是我的事这种乐于助人的良好品质。JavaScript 也有许多事件要处理,接着就演示下,如何在 DOM 元素中选出一位热心肠的好同学。
场景
想象一个平平无奇的表格,像下面这样:

需求是点击删除按钮可删除单条数据。
用 JavaScript 实现点击删除的操作,需要监听按钮点击事件,在事件点击回调中,发送删除数据的请求,请求成功后重新渲染表格。
基本实现
基本款的实现大概如下:
HTML:
...
<tr>
<td>1</td>
<td>汉堡</td>
<td>
<button data-id="1">删除</button>
</td>
</tr>
...
JavaScript:
const deleteItem = (id) => {
// 依据 ID 删除数据。
}
let btnList = document.getElementsByClassName('btn')
Array.prototype.forEach.call(btnList, (btn) => {
btn.addEventListener('click', function (e) {
deleteItem(this.dataset.id)
})
})
上面的代码给每一条删除按钮都添加了点击事件的回调。上面的代码利用自定义属性 data-id
将单条数据的 ID
传给数据的删除按钮,然后在点击事件的回调中通过 dataset
属性取得,将其传递给处理删除的函数。
这样可以实现上面的上面所说的删除功能。
副作用
可是这么写,当数据一多,将会对页面性能造成影响。
参考《JavaScript 高级程序设计》:
首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。
面对这种状况,解决方案是事件代理。
事件代理实现
先看代码:
let table = document.getElementById('table')
table.addEventListener('click', function (e) {
if (e.target.tagName === 'BUTTON') {
deleteItem(e.target.dataset.id)
}
})
上面的代码,首先在回调中通过事件对象 e
的 target
属性取得触发点击事件的元素,然后通过 tagName
判断这个元素是不是按钮,如果是的话,就进行删除操作。
只添加了一次事件处理函数,就实现了功能。
这里利用的是事件流的事件冒泡。
原理
事件流
事件流有三个阶段,分别是事件捕获阶段,处于目标阶段,还有事件冒泡阶段。
按钮点击事件的事件流如下:

上图省略了 tr
,td
, tbody
这些元素,只用关键的元素来展示事件流。
“事件捕获”是不太精确的目标先接收到事件,首先是文档,然后再到 body
,逐渐精确到最具体的节点 button
.
当 button
接收到事件,即为“处于目标阶段”。
之后,事件再向外层逐级传播到不太具体的节点,这便是“事件冒泡阶段”。
冒泡与捕获演示
假如有个外层元素称为 box
, 它里面包含了一个按钮 btn
:

给 box
和 btn
注册点击事件,代码如下:
box.addEventListener('click', function (e) {
console.log('box')
})
btn.addEventListener('click', function (e) {
console.log('button')
})
点击 btn
:

可以看到,首先打印的是 'button'
,接着才是 box
. 因为 addEventListener
默认是在冒泡阶段执行回调函数的。这里,最具体的 btn
接受到事件之后,向外层冒泡,box
才能接受到事件。
改一下代码:
box.addEventListener('click', function (e) {
console.log('box')
}, true)
btn.addEventListener('click', function (e) {
console.log('button')
})
这里给 box
的 addEventListener
传入了第三个参数 true
, 表示要在捕获阶段处理 box
的点击事件。
addEventListener
第三个参数形参为 useCapture
, 表示是否在捕获阶段处理事件,默认为 false
.
运行一下:

这样一改,首先打印的就是 'box'
, 因为事件具体节点是 btn
,而事件要从不太具体的节点,经过捕获阶段,才确定到 btn
. 在捕获阶段,外层 box
要比 btn
早接受到事件,所以先执行了 box
捕获阶段的事件处理函数。
总结
事件代理的代码中,注册点击事件的是 table
元素,发生点击事件最具体的节点是 button
. 事件在 button
发生后,由于冒泡,会传播到 table
, 从而触发 table
上事先注册的回调函数。所以,用事件冒泡实现事件代理,可以只给热心肠同学 table
添加点击事件回调,而不必劳烦表格中的每个 button
.
在类似的场景下,可利用事件代理,来减少冗余,节约资源。