- 事件冒泡
- 事件捕获
- 事件流
- 事件监听
- 事件委托/事件代理
事件冒泡和事件捕获
事件冒泡和事件捕获分别由微软和Netscape团队提出,这两个概念都是为了解决页面中事件流(事件发生顺序)的问题。
例如:
<div @click="click1">
<button @click="click2">点击</button>
</div>
那么点击按钮的时候,是先触发click1还是先触发click2?
事件冒泡
微软提出了名为事件冒泡(event bubbling)的事件流。事件被定义为从最具体的元素(文档树中最深的节点)开始触发,然后向上传播至没有那么具体的元素(文档)
因此在事件冒泡的概念下在button元素上发生click事件的顺序应该是button -> div -> body -> html -> document
事件捕获
Netscape Communicator团队提出了另一种名为事件捕获的事件流。事件捕获的意思是最不具体的节点应该最先收到事件,而最具体的节点应该最后收到事件。与事件冒泡相反,从最外层开始发生到最具体的元素。
因此在事件捕获的概念下在button元素上发生click事件的顺序应该是document -> html -> body -> div -> button
事件流
HTML中与javascript交互是通过事件驱动来实现的,例如鼠标点击事件onclick、页面 的滚动事件onscroll 等等,可以向文档或者文档中的元素添加事件侦听器来预订事件。 想要知道这些事件是在什么时候进行调用的,就需要了解一下“事件流”的概念。
什么是事件流?
事件流描述的是从页面中接收事件的顺序,DOM2 级事件流包括下面几个阶段。
- 事件捕获阶段
- 到达目标阶段
- 事件冒泡阶段
先捕获后冒泡,在DOM事件流中,实际的目标在捕获阶段不会接收到事件
事件监听
简单使用
addEventListener:addEventListener是DOM2 级事件新增的指定事件处理程序的操作, 这个方法接收3个参数:
- 要处理的事件名、
- 作为事件处理程序的函数
- 一个布尔值。如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示 在冒泡阶段调用事件处理程序。IE只支持事件冒泡。【第三个参数不填默认false,事件冒泡】
- 在冒泡中,内部元素先被触发,然后再触发外部元素,
- 捕获中,外部元素先被触发,在触发内部元素。
event
事件模型有三个常用方法:
- event.stopPropagation:阻止捕获和冒泡阶段中,当前事件的进一步传播
- event.stopImmediatePropagetion,阻止调用相同事件的其他侦听器
- event.preventDefault,取消该事件(假如事件是可取消的)而不停止事件的进一步传播。比如在a标签的绑定事件上调用此方法,链接则不会被打开,但是会发生冒泡,冒泡会传递到上一层的父元素;
- event.target:指向触发事件的元素,在事件冒泡过程中这个值不变
- event.currentTarget = this,时间帮顶的当前元素,只有被点击时目标元素的target 才会等 于currentTarget
1.stopPropagation()阻止事件进一步传播
2.stopImmediatePropagetion()阻止调用相同事件的其他侦听器
对比stopPropagation()
3.preventDefault()取消该事件,但不停止事件进一步传播
事件委托/事件代理
事件代理(Event Delegation)也称之为事件委托。是JavaScript中常用绑定事件的常用技巧。
顾名思义,“事件代理”即是把原本需要绑定在子元素的响应事件委托给父元素,让父元素担当事件监听的职务。
事件代理的原理是DOM元素的事件冒泡。
事件委托的优点
- 可以大量节省内存占用,减少事件注册
- 可以实现当新增子对象时无需再次对其绑定(动态绑定事件)
- 节省花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省 DOM引用,也可以节省时间。
普通示例
如果我们有一个列表,列表之中有大量的列表项,我们需要在点击列表项的时候响应一个事件
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的
// 获取目标元素
const lis = document.getElementsByTagName("li")
// 循环遍历绑定事件
for (let i = 0; i < lis.length; i++) {
lis[i].onclick = function(e){
console.log(e.target.innerHTML)
}
}
这时候就可以事件委托,把点击事件绑定在父级元素ul上面,然后执行事件的时候再去匹配目标元素
// 给父层元素绑定事件
document.getElementById('list').addEventListener('click', function (e) {
// 兼容性处理
var event = e || window.event;
var target = event.target || event.srcElement;
// 判断是否匹配目标元素
if (target.nodeName.toLocaleLowerCase === 'li') {
console.log('the content is: ', target.innerHTML);
}
});
但现实中更多的是动态增加或者去除列表项元素,使用事件委托更加方便
事件委托动态增加或者去除
<input type="button" name="" id="btn" value="添加" />
<ul id="ul1">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
<li>item 4</li>
</ul>
使用事件委托
const oBtn = document.getElementById("btn");
const oUl = document.getElementById("ul1");
const num = 4;
//事件委托,添加的子元素也有事件
oUl.onclick = function (ev) {
ev = ev || window.event;
const target = ev.target || ev.srcElement;
if (target.nodeName.toLowerCase() == 'li') {
console.log('the content is: ', target.innerHTML);
}
};
//添加新节点
oBtn.onclick = function () {
num++;
const oLi = document.createElement('li');
oLi.innerHTML = `item ${num}`;
oUl.appendChild(oLi);
};
可以看到,使用事件委托,在动态绑定事件的情况下是可以减少很多重复工作的
事件委托小结
使用事件委托注意事项:使用事件委托时,并不是说把事件委托给的元素越靠近顶层就越好。事件冒泡的过程也需要耗时,越靠近顶层,事件的”事件传播链”越长,也就越耗时。如果DOM嵌套结构很深,事件冒泡通过大量祖先元素会导致性能损失。
适合事件委托的事件有:click,mousedown,mouseup,keydown,keyup,keypress
从上面应用场景中,我们就可以看到使用事件委托存在两大优点:
- 减少整个页面所需的内存,提升整体性能
- 动态绑定,减少重复工作
但是使用事件委托也是存在局限性:
focus、blur这些事件没有事件冒泡机制,所以无法进行委托绑定事件mousemove、mouseout这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的
如果把所有事件都用事件代理,可能会出现事件误判,即本不该被触发的事件被绑定上了事件
参考: 前端面试题宝典
Javascript高级程序设计(第四版)