关于事件代理(事件委托)
由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方法叫做事件的代理(delegation)。
一、事件代理的定义
1、什么是事件代理
事件代理(事件委托)就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
2、原理
事件代理的原理是给父元素添加侦听器,利用事件冒泡影响每一个子元素。
3、作用
- 事件代理只需操作一次 DOM,可以减少内存消耗,提高性能;
- 事件代理会进行动态绑定事件,对子元素增加或删除不用重新绑定事件。
二、事件代理的应用
1、什么时候需要事件代理
当要给一组元素添加相同的事件时,可以直接添加给父元素,进行事件代理。
举个例子,假设有一个列表,列表之中有大量的列表项,我们需要在点击每个列表项的时候响应一个事件:
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
如果给每个列表项都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。
借助事件代理,我们只需要给父容器 ul 绑定方法即可,这样不管点击的是哪一个后代元素,都会根据冒泡传播的传递机制,把容器的 click 行为触发,然后把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而完成不同的事。
很多时候,我们需要通过用户操作动态的增删列表项元素,如果一开始给每个子元素绑定事件,那么在列表发生变化时,就需要重新给新增的元素绑定事件,给即将删去的元素解绑事件,如果用事件代理就会省去很多这样的麻烦。
2、如何实现事件代理
接下来我们来实现上例中父层元素 #list 下的 li 元素的事件委托到它的父层元素上:
// 给父层元素绑定事件
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);
}
});
三、Event 对象
1、target 和 currentTarget
- target 是指触发事件的元素。
- currenttarget 是指事件绑定的元素。
举个例子:
<div id="a">
<div id="b">
<div id="c">
<div id="d"></div>
</div>
</div>
</div>
<script>
document.getElementById('a').addEventListener('click', function(e) {
console.log(
'target: ' + e.target.id + ' currentTarget: ' + e.currentTarget.id
)
})
document.getElementById('b').addEventListener('click', function(e) {
console.log(
'target: ' + e.target.id + ' currentTarget: ' + e.currentTarget.id
)
})
document.getElementById('c').addEventListener('click', function(e) {
console.log(
'target: ' + e.target.id + ' currentTarget: ' + e.currentTarget.id
)
})
document.getElementById('d').addEventListener('click', function(e) {
console.log(
'target: ' + e.target.id + ' currentTarget: ' + e.currentTarget.id
)
})
</script>
当点击最里层的元素 d 的时候,会依次输出:
- target: d currentTarget: d
- target: d currentTarget: c
- target: d currentTarget: b
- target: d currentTarget: a
可以看到,event.target
指向引起触发事件的元素,而 event.currentTarget 则是事件绑定的元素,只有被点击的那个目标元素的 event.target
才会等于 event.currentTarget。
也就是说,event.currentTarget 始终是监听事件者,而 event.target
是事件的真正发出者,二者在没有事件冒泡的情况下是一样的。
2、stopPropagation 和 stopImmediatePropagation
event.stopPropagation() 与 event.stopImmediatePropagation()
的区别在于 stopImmediatePropagation
既能阻止事件向父元素冒泡,也能阻止元素同事件类型的其它监听器被触发。而 stopPropagation 只能实现前者的效果。
举个例子:
<body>
<button id="btn">click me to stop propagation</button>
</body>
<script>
const btn = document.querySelector('#btn');
btn.addEventListener('click', event => {
console.log('btn click 1');
event.stopImmediatePropagation();
});
btn.addEventListener('click', event => {
console.log('btn click 2');
});
document.body.addEventListener('click', () => {
console.log('body click');
});
// btn click 1
</script>
上面代码中,使用 stopImmediatePropagation
后,点击按钮时,不仅 body 绑定事件不会触发,与此同时按钮的另一个点击事件也不触发。
3、preventDefault
preventDefault
用于阻止默认事件行为。
举个例子:
// 阻止鼠标右键弹出的默认菜单
window.oncontextmenu = function (event) {
event.preventDefault();
};
四、封装通用的事件绑定函数
一个好的 addEvent()
方法应当达到以下要求:
- 支持同一元素的同一事件句柄可以绑定多个监听函数;
- 如果在同一元素的同一事件句柄上多次注册同一函数,那么第一次注册后的所有注册都被忽略;
- 函数体内的 this 指向的应当是正在处理事件的节点(如当前正在运行事件句柄的节点);
- 监听函数的执行顺序应当是按照绑定的顺序执行;
- 在函数体内不用使用
event = event || window.event
来标准化 Event 对象;
- 封装
- 使用
/**
* @param {ele}:dom 元素,给哪个 dom 元素添加事件
* @param {type}:事件类型
* @param {selecter}:选择器,根据这个参数判断是否需要事件代理
* @param {fn}:执行函数
*/
function bindEvent(ele, type, selector, fn) { // 元素, 事件类型, 选择器, 调用函数
if (fn === null) {
// 只有 3 个参数时,selector 对应 fn 函数,在这更改赋值
fn = selector
// 赋值完毕后,清空 selector
selector = null
}
// 这里使用匿名函数,防止箭头函数的 this 问题
ele.addEventListener(type, e => {
let target
if (selector) {
// 如果有 selector,则为事件代理,获取实际触发事件的元素
target = e.target
// 如果当前传入的 selector 是触发事件的 target 的选择器
if (target.matches(selector)) {
// 更改 fn,即事件触发时 fn 函数的 this 指向为当前触发元素的 this
fn.call(target, e)
}
} else {
// 不需要代理
fn.call(target, e)
}
})
}
<body>
<div>
<div id="div1">
<a href="#">a1</a>
<a href="#">a2</a>
<a href="">a3</a>
<a href="">a4</a>
<a href="">a5</a>
</div>
</div>
<script>
function bindEvent(ele, type, selector, fn) {
if (fn === null) {
fn = selector
selector = null
}
ele.addEventListener(type, e => {
let target
if (selector) {
target = e.target
if (target.matches(selector)) {
fn.call(target, e)
}
} else {
fn.call(target, e)
}
})
}
var div1 = document.getElementById('div1')
// 注意,这里的函数不能使用箭头函数,箭头函数没有 this 指向,会向上找到 window
bindEvent(div1, 'click', 'a', function (e) {
e.preventDefault() // 取消默认行为,例如这里的 a 取消默认的跳转行为
// 这里使用了 this,所以函数中需要 call() 改变 this 指向
console.log(this.innerHTML)
})
</script>
</body>