由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(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.targetif (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.targetif (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//看看被操作的元素是否符合buttonwhile (!el.matches(selector)) {//如果在找的时候一直找到了div1,就认为找不到了,判定为空,跳出if (element === el) {el = nullbreak}//不符合就让这个元素等于父元素el = el.parentNode}//如果找到了就调用fn,第一个参数传e,第二个传匹配到的元素el && fn.call(el, e, el)})return element}
