Skip to main content

关于事件代理(事件委托)

由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方法叫做事件的代理(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)
}
})
}