一、JS事件传播机制

在JS中,一个事件触发后,会在子元素和父元素之间传播,这个过程分为三个阶段

  • 捕获阶段:通俗的理解就是,当鼠标点击或者触发dom事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件。
  • 目标阶段:事件对象传递到事件目标。如果事件的type属性表明后面不会进行冒泡操作,那么事件到此就结束了。
  • 冒泡阶段:与事件捕获恰恰相反,事件冒泡顺序是由内到外进行事件传播,直到根节点。

1.示例引入

我们先来看一段代码

html部分

1
2
3
4
5
6
<!-- 在此省略css样式部分 -->
<div id="granpa">
<div id="dad">
<div id="son"></div>
</div>
</div>

js部分

1
2
3
4
5
6
7
8
9
10
function printID(){
console.log(this.id);
}
let grandpa = document.getElementById('grandpa')
let dad = document.getElementById('dad')
let son = document.getElementById('son')
//使用addEventListener三个元素添加click事件
grandpa.addEventListener('click',printID)
dad.addEventListener('click',printID)
son.addEventListener('click',printID)

2023-04-19_074832

此时当我们点击不同元素时

  • 点击son(即黑色盒子),依次打印 son - dad -grandpa。表明默认是内到外进行事件传播
  • 点击grandpa(即红色盒子),只打印 granpa。

JS中默认是在冒泡阶段以此触发事件的,这里也可以在addEventListener函数中传入第三个参数true(默认为false),这样事件就会在捕获阶段触发了。

2.event对象

在上面的例子中,我们使用了this,其指向绑定事件的对象。但在JS中还有一个专门的对象event,当dom 树中某个事件被触发的时候,会同时自动event对象,它可以描述事件所有的相关信息(比如事件在其中发生的元素、键盘按键的状态、鼠标的位置、鼠标按钮的状态。)的对象。

这个event到底是个啥,我们可以先来输出看一看,

1
grandpa.addEventListener('click',event => console.log(event))

之后我们会在控制台看到一个PointerEvent对象(可从MDN中详细查看),里面包含了一大推各种各样的属性,只需记住常用的几个就行。

其中有两个属性target与currentTarget,我们先来看一看。

1
2
3
grandpa.addEventListener('click',event => console.log(event.currentTarget.id,event.target.id))
dad.addEventListener('click',event => console.log(event.currentTarget.id,event.target.id))
son.addEventListener('click',event => console.log(event.currentTarget.id,event.target.id))

验一下吧,当我们点击son元素时,会依次打印

son son

dad son

grandpa son

也不卖关子了,event.currentTarget表示触发事件的对象,event.target表示最初触发事件的对象。了解了JS的事件机制后,我们知道,事件有一个冒泡阶段,当子元素的事件触发时,其祖先元素上的相同事件也会同时被触发。这样我们就能理解上面输出的含义了,我们点击的是son元素,尽管父元素也会跟着触发,但整个事件是由son元素最初触发引起的,因此target始终为son元素。

此外,我们也注意到,在最开始示例中的this与event.currentTarget都表示事件绑定的对象。但有一点需要注意,箭头函数是没有自身的this的,因此我们可以这么说,当使用普通函数而非箭头函数时,二者是等价的。

二、事件委托

事件委托就是把原本需要绑定在子元素上的事件委托给它的父元素,让父元素来监听子元素的冒泡事件,并在子元素发生事件冒泡时找到这个子元素。

1.为什么用事件委托

  • 节省监听数,也就是节省内存

在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能,因为需要不断的与dom节点进行交互,访问dom的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少DOM操作的原因;每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率越大。

假设,我们有一个列表,列表中有大量的列表项,我们需要在点击列表项的时候响应一个事件

1
2
3
4
5
6
7
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>

方法一:利用循环,为ul下的所有的li都绑定一个click事件(这样太傻了,对内存消耗很大)

方法二:利用事件冒泡,为li的父元素ul绑定一个click事件,然后在执行事件的时候再去匹配目标元素

  • 可以动态绑定事件

    想一想,很多时候,我们是需要从服务端获取数据并动态添加元素的,假设我们没有使用事件委托,为所有li单独绑定了事件,现在获取数据之后,新创建了li元素,那我们岂不是重写为新增的元素绑定事件。这样可太麻烦了。

    以上面的html代码为例(省略样式),我们来看一下如何使用事件委托

    1
    2
    3
    4
    5
    6
    7
    8
    // 给父层元素绑定事件
    let list = document.getElementById('list').
    list.addEventListener('click', function (e) {
    // 判断是否匹配目标元素
    if (e.target.nodeName.toLocaleLowerCase() === 'li') {
    console.log('the content is: ', e.target.innerText);
    }
    });

    看上去是不是十分简洁

我们来总结一下事件委托的基本方法,首先将事件绑定绑定给父元素,然后匹配目标元素。当时匹配的方法也就很多了,可以通过标签,类名,id等多种方式。

2 小练习

在了解了JS 的事件委托后,我们来练习一下吧。要求:创建一个表格,用户可以添加并删除对应的行

html部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<table width="400px" frame='hsides' rules="rows">
<!-- -->
<thead>
<td>姓名</td>
<td>年龄</td>
<td>操作</td>
</thead>
<tbody id="tbody">
</tbody>
</table>
<form action="">
<input type="text" id="iptName" placeholder="请输入姓名">
<input type="number" id="iptAge" placeholder="请输入年龄">
</form>
<button id="btn">添加人员</button>

JS部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//模拟获取到的数据
let data = [
{id:1,name:"张三",age:18},
{id:2,name:"李四",age:19},
{id:3,name:"李梅",age:18}]
let rows=[]
let tbody = document.getElementById('tbody');
// 初始化表格
(function creatList() {
data.forEach(per => {
rows.push('<tr><td>' + per.name + '</td><td>' + per.age + '<td><a class="del" data-id='+per.id+' href="javascript:;">删除</a></td></td></tr>')
});
tbody.insertAdjacentHTML("afterBegin",rows.join(''))
})();

// 点击按钮添加新值
let btn = document.getElementById("btn")
btn.onclick=function(){
let perName = document.getElementById('iptName').value
let perAge = document.getElementById('iptAge').value
let perId = document.getElementById('tbody').children.length
let per = {id:perId,name:perName,age:perAge}
if(!perAge || !perName){
return alert("不能为空值")
}
insetHtml = '<tr><td>' + per.name + '</td><td>' + per.age + '<td><a class="del" data-id='+per.id+' href="javascript:;">删除</a></td></td></tr>'
tbody.insertAdjacentHTML("beforeEnd",insetHtml)
}

// 删除某一值
tbody.addEventListener('click',(e)=>{
if (e.target.className=='del') {
let tr = e.target.parentNode.parentNode
tr.remove()
}
})