JavaScript 事件代理简单例子

675 阅读4分钟

“代理”可以理解为代劳或者帮忙,或者类似你的事就是我的事这种乐于助人的良好品质。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)
  }
})

上面的代码,首先在回调中通过事件对象 etarget 属性取得触发点击事件的元素,然后通过 tagName 判断这个元素是不是按钮,如果是的话,就进行删除操作。

只添加了一次事件处理函数,就实现了功能。

这里利用的是事件流的事件冒泡。

原理

事件流

事件流有三个阶段,分别是事件捕获阶段,处于目标阶段,还有事件冒泡阶段。

按钮点击事件的事件流如下:

上图省略了 tr ,td, tbody 这些元素,只用关键的元素来展示事件流。

“事件捕获”是不太精确的目标先接收到事件,首先是文档,然后再到 body,逐渐精确到最具体的节点 button.

button 接收到事件,即为“处于目标阶段”。

之后,事件再向外层逐级传播到不太具体的节点,这便是“事件冒泡阶段”。

冒泡与捕获演示

假如有个外层元素称为 box, 它里面包含了一个按钮 btn

boxbtn 注册点击事件,代码如下:

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')
})

这里给 boxaddEventListener 传入了第三个参数 true, 表示要在捕获阶段处理 box 的点击事件。

addEventListener 第三个参数形参为 useCapture, 表示是否在捕获阶段处理事件,默认为 false.

运行一下:

这样一改,首先打印的就是 'box', 因为事件具体节点是 btn,而事件要从不太具体的节点,经过捕获阶段,才确定到 btn. 在捕获阶段,外层 box 要比 btn 早接受到事件,所以先执行了 box 捕获阶段的事件处理函数。

总结

事件代理的代码中,注册点击事件的是 table 元素,发生点击事件最具体的节点是 button. 事件在 button 发生后,由于冒泡,会传播到 table, 从而触发 table 上事先注册的回调函数。所以,用事件冒泡实现事件代理,可以只给热心肠同学 table 添加点击事件回调,而不必劳烦表格中的每个 button .

在类似的场景下,可利用事件代理,来减少冗余,节约资源。