由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。
这里摘用一下凌云之翼对事件委托的例子。
有三个同事预计会在周一收到快递。为签收快递,有两种办法:一是三个人在公司门口等快递;二是委托给前台MM代为签收。现实当中,我们大都采用委托的方案(公司也不会容忍那么多员工站在门口就为了等快递)。前台MM收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优势,那就是即使公司里来了新员工(不管多少),前台MM也会在收到寄给新员工的快递后核实并代为签收。 这里其实还有2层意思的: 第一,现在委托前台的同事是可以代为签收的,即程序中的现有的dom节点是有事件的; 第二,新员工也是可以被前台MM代为签收的,即程序中新添加的dom节点也是有事件的。
事件委托的优点
省监听的内存
如果要给100个按钮添加点击事件,我们只需要用事件委托监听这100个按钮的祖先,等冒泡的时候判断target是不是这100个按钮中的一个,这样我们就需要一个内存空间就够了,自然性能也会更好。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>
<div id="div1">
<button data-id="1">click 1</button>
<button>click 2</button>
<button>click 3</button>
//...
<button>click 100</button>
</div>
</body>
</html>
div1.addEventListener('click', (e) => {
const t = e.target
if (t.tagName.toLowerCase() === 'button') {
console.log('button 被点击了')
console.log('button 内容是' + t.textContent)
console.log('button data-id是' + t.dataset.id) //dataset获取以data开头的属性的值
}
})
可以监听动态元素
如果有一个button在一秒钟之后出现,那我们该怎么监听他,监听祖先,等点击的时候看看是不是我想要监听的元素即可。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>
<div id="div1">
</div>
</body>
</html>
setTimeout(() => {
const button = document.createElement('button')
button.textContent = 'click 1'
div1.appendChild(button)
}, 1000)
div1.addEventListener('click', (e) => {
const t = e.target
if (t.tagName.toLowerCase() === 'button') {
console.log('button 被click')
}
})
封装事件委托
写出这样一个函数on('click','#testDiv','li',fn)
,当用户点击#testDiv里的li元素时,调用fn函数,要求用到事件委托。
答案一
判断target是否匹配’li’
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>
<div id="div1">
</div>
</body>
</html>
setTimeout(() => {
const button = document.createElement('button')
button.textContent = 'click 1'
div1.appendChild(button)
}, 1000)
//在div1上做事件委托看button有没有被点击
on('click', '#div1', 'button', () => {
console.log('button 被点击了')
})
//输入(事件类型,元素,选择器/准备匹配什么元素,回调函数)
function on(eventType, element, selector, fn) {
if (!(element instanceof Element)) {
element = document.querySelector(element)
}
element.addEventListener(eventType, (e) => {
const t = e.target
//判断一个元素是否满足一个选择器
if (t.matches(selector)) {
fn(e)
}
})
}
答案二
如果button里面还有个span,span的内容是chick 1,点击chick 1 实际操作的元素是span,就不能通过button被点击了。
要用递归判断,如果当前元素不匹配button,就向自己的祖辈寻找,如果祖辈里有button就说明点击了button。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>
<div id="div1">
</div>
</body>
</html>
setTimeout(() => {
const button = document.createElement('button')
const span = document.createElement('span')
span.textContent = 'click 1'
button.appendChild(span)
div1.appendChild(button)
}, 1000)
//在div1上做事件委托看button有没有被点击
on('click', '#div1', 'button', () => {
console.log('button 被点击了')
})
//输入(事件类型,元素,选择器/准备匹配什么元素,回调函数)
function on(eventType, element, selector, fn) {
if (!(element instanceof Element)) {
element = document.querySelector(element)
}
element.addEventListener(eventType, e => {
let el = e.target
//看看被操作的元素是否符合button
while (!el.matches(selector)) {
//如果在找的时候一直找到了div1,就认为找不到了,判定为空,跳出
if (element === el) {
el = null
break
}
//不符合就让这个元素等于父元素
el = el.parentNode
}
//如果找到了就调用fn,第一个参数传e,第二个传匹配到的元素
el && fn.call(el, e, el)
})
return element
}