一、基本概念
要想了解事件委托,必先了解事件流
事件流即事件的发生顺序,阶段分为:
1. 捕获阶段:从根节点开始顺着目标节点构建一条事件路径,首先 window
会获捕获到事件,之后 document
、html
、body
会捕获到,再之后就是在 body
中 DOM 元素一层一层的捕获到事件2. 目标阶段:到达目标节点,即元素本身3. 冒泡阶段:从目标节点顺着捕获阶段构建的路径返回去, 逐级向上,直至 window
如图 👇
1~5 是捕获阶段,5~6 是目标阶段,6~10 是冒泡阶段;
应用最多的是
冒泡阶段
,比如事件委托
二、事件委托
事件委托又叫事件代理,利用了事件的冒泡机制,将节点上的事件委托给上层节点处理。
一个栗子:
有三个同事预计会在周一收到快递,为了签收快递,有两种办法:方法 1. 三个人在公司门口等快递方法 2. 等快递到了让前台妹子代为签收
第一种方法:缺点是每个人都要在门口等着,如果人数变为十个,二十个那公司也不会容忍那么多员工站在门口就为了等快递
第二种方法:前台妹子收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优点,那就是即使公司里来了新员工,前台妹子也可以在收到寄给新员工的快递后核实并代为签收。
第二种方法就是事件委托,翻译过来就是:1. 前台妹子可以代收同事们的快递,即能获取到现有的 DOM 节点的事件2. 新员工的快递也是可以被前台妹子代为签收,即也能获取到程序中新添加的 DOM 节点的事件
三、为什么要有事件委托
如果 DOM 需要有事件处理程序,那我们直接给它设事件处理程序就好了
如果是很多的 DOM 需要添加事件处理呢?比如我们有 5 个 li
,每个 li
都有相同的 click
点击事件,那我们直接用 for
循环的方法,来遍历所有的 li
,然后给它们添加事件,请看👇:
<ul id='ul'>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
const ul = document.querySelector("ul");
const liList = ul.getElementsByTagName('li');
for (let i = 0; i < liList.length; i++) {
liList[i].addEventLister('click', (e) => {
console.log(`点击了${e.target.innerHTML}, 索引为 ${i}`);
});
}
弊端:
1. 与 DOM 的交互增多:添加到页面上的事件数量将直接关系到页面的整体运行性能,因为需要不断的与 DOM 节点进行交互,访问 dom 的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少 DOM 操作的原因;如果要用事件委托,就会将所有的操作放到 js 程序里面,与 DOM 的操作就只需要交互一次,这样就能大大的减少与 DOM 的交互次数,提高性能;
2. 内存占用大:每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就越大,自然性能就越差了,比如上面的 5 个 li,就要占用 5 个内存空间,如果是 1000 个,10000 个呢,如果用事件委托,那么我们就可以只对它的父级(如果只有一个父级)这一个对象进行操作,这样我们就需要一个内存空间就够了
3. 新增加的 DOM 元素无法获取事件
用事件委托可以完美规避上面三个弊端,那么问题来了,怎么用事件委托实现呢,请看👇
const ul = document.querySelector('ul');
ul.addEventLister("click", e => {
const { target } = e;
if (target.tagName.toLowerCase() === 'li') {
const liList = this.querySelectorAll('li');
const index = Array.prototype.indexOf.call(liList, target); // 因为 liList 不是真正的数组,是节点集合,对于非真正数组,可以通过 call 来改变 this 指向,调用 Array 原型链上的各种方法
console.log(`点击了${target.innerHTML}, 索引为 ${index}`);
}
});
四、面试官问
问题一:
问题描述:window.addEventListerner
监听的是什么阶段的事件 ?
答:要看第三个参数的值
1. 如果第三个参数为 true
,则为捕获阶段
window.addEventLister('click', () => {
// todo
}, true);
2. 如果第三个参数为 false
,或者没传(默认值为 false
),则为冒泡阶段
window.addEventLister('click', () => {
// todo
});
问题二:
问题描述:页面上,有若干按钮,每个按钮都有自身的点击事件需求是,每个用户有个是否被封禁的属性 banned = true
,封禁用户点击页面上任何按钮都不可响应原来的函数,而是直接 alert
出提示信息:'你被封禁了'
方法 1: 在每个 click
事件里加个判断
if (banned) {
alert('你被封禁了');
return;
}
缺点:1. 改动较多,冗余代码多,不优雅2. 新增加的 DOM 节点无此判断
方法 2: 如果是封禁用户,在整个页面上加一层透明的遮罩,并添加 click
事件,alert
出 '你被封禁了',这样点击的时候就只会点击到这层遮罩上。 缺点:需要额外添加页面元素
方法 3: 在最顶层监听捕获的阶段
window.addEventLister(
"click",
(e) => {
if (banned) {
e.stopProgagtion();
alert('你被封禁了');
}
},
true
);
五、事件委托的使用场景
1. 适合用事件委托的有:click
,mousedown
,mouseup
,keydown
,keyup
,keypress
2. 不适合用事件委托的有:focus
,blur
之类的,本身就没用冒泡的特性,所以就无法用事件委托了
值得注意的是,
mouseover
和mouseout
虽然也有事件冒泡,但是处理它们的时候需要特别的注意,因为需要经常计算它们的位置,处理起来不太容易。特别是mousemove
,每次都要计算它的位置,非常不好把控,建议不要用