在JavaScript中,事件委托Event delegation是一种事件的响应机制,当需要监听不存在的元素或是动态生成的元素时,可以考虑事件委托。

事件委托得益于事件冒泡(有关事件冒泡可以参考事件冒泡与事件捕获),当监听子元素时,事件冒泡会通过目标元素向上传递到父级,直到document,如果子元素不确定或者动态生成,可以通过监听父元素来取代监听子元素。

举个例子:
假设页面从存在一个ul,其子元素动态生成.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>事件委托</title>
</head>
<body>
<button>点击</button>
<ol></ol>
<script>
let btn = document.getElementsByTagName('button')[0]
let ol = document.getElementsByTagName('ol')[0]
btn.addEventListener('click',function(){
let li = document.createElement('li')
let number = parseInt(Math.random() * 100, 10)
li.textContent=number
ol.appendChild(li)
})
<script>
</body>
</html>

当点击按钮时,ol列表中动态创建li,如果给li添加click事件,当li被点击时,li本身被删除,那么可以这样实现

1
2
3
4
5
6
7
8
9
btn.addEventListener('click',function(){
let li = document.createElement('li')
let number = parseInt(Math.random() * 100, 10)
li.textContent=number
ol.appendChild(li)
li.addEventListener('click',function(){
li.remove();
})
})

但是这样就会使得给每个li都添加了click事件(或者其他的事件),而页面中的li是不确定有多少的,如果页面中生成很多的li,那么就会造成内存占用过多,因此我们可以使用事件委托来解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
/*
li.addEventListener('click',function(){
li.remove();
})
*/
ol.addEventListener('click',function(e){
let ele = e.target
if(ele.tagName==='LI'){
ele.remove()
}
})

在这个例子中,通过给父级而非子集添加事件,当事件被抛到更上层的父节点的时候,我们通过检查事件的目标对象target来判断并获取事件源li,当子节点被点击的时候,click事件会从子节点开始向上冒泡。父节点捕获到事件之后,通过判断e.target.nodeName来判断是否为我们需要处理的节点。并且通过e.target拿到了被点击的Li节点。从而可以获取到相应的信息,并作处理。

但是当li中有其他的标签时如span标签(或是其他的标签),直接使用上述方法就会导致事件无法被正确触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
btn.addEventListener('click',function(){
let li = document.createElement('li')
let span = document.createElement('span')
let number = parseInt(Math.random() * 100, 10)
span.textContent=number
li.appendChild(span)
ol.appendChild(li)
ol.addEventListener('click',function(e){
let ele = e.target
if(ele.tagName==='LI'){
ele.remove()
}
})
})

此时,当点击span时,li并未被删除,点击spanli之间的部分li才会被删除,试想当子元素li中有多层嵌套时,就会导致事件无法被正确触发,因此我们需要对li进行判断,判断点击的是否是ol下的li,否则就继续查找,直到查找到li

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
btn.addEventListener('click',function(){
let li = document.createElement('li')
let span = document.createElement('span')
let number = parseInt(Math.random() * 100, 10)
span.textContent=number
li.appendChild(span)
ol.appendChild(li)
ol.addEventListener('click',function(e){
let ele = e.target
while(ele.tagName!=='LI'){
if(ele===ol){
ele=null; break;
}
ele=ele.parentNode
}
ele && ele.remove()
})
})

此时不论li嵌套多少层,liclick事件都会被正确触发。

事件委托可以通过监听父级来达到监听子级的效果,减少了监听器的数量,使用内存也相对减少。当在一些难以追踪的情况下,从DOM从删除的元素仍保留「记忆」(即泄露),这些泄露的「记忆」通常与事件绑定联系在一起,使用事件委托,可以随意的对子元素进行事件绑定而不用担心忘记解绑它们的监听器。

参考:

  1. What is DOM Event delegation?
  2. How JavaScript Event Delegation Works