写在前面
DOM 文档编程接口中并没有为 DOM元素 提供原生的事件委托接口。对于 DOM 中的事件机制,原生 DOM 仅仅提供了事件模型的接口:事件监听与处理。事件监听与处理目前是有两种方式实现,即 on{eventtype} 和 addEventListener。具体参看上篇博客 最全的DOM事件笔记
事件委托思想是程序员在实际需求中发掘出来的。
在 原生DOM 提供的 DOM事件对象 的基础上,有好几种实现事件委托的方法。比如 JQuery 库中封装好的 on() 方法。那么下面介绍几种在原生 DOM语法 中的事件委托的实现方法。
假设场景如下:
<div class="grandpa"><div class="father"><div class="child"><span class="text">文字</span></div></div></div>
需求如下:
对儿子元素(.child)进行 click 事件监听,当该元素被点击时,在控制台输出 我是儿子,我被点击了! 。
1. 常见但错误的方法
const grandpaEl = document.getElementsByClassName('grandpa')[0];grandpaEl.addEventListener('click', (e)=>{const el = e.target;if(el.className === 'child'){console.log('我是儿子,我被点击了!');}})
这样得到的结果是,当点击 .child 元素时会正确输出,但当点击 .child 里面的 .text 元素时不会正确输出。
根据我们的实际需求,当点击父元素包裹的子元素时,也是相当于点击了父元素,应当触发对应的事件处理函数。因此此种方法严格来说是错误的。
2. 递归方法
const grandpaEl = document.getElementsByClassName('grandpa')[0];grandpaEl.addEventListener('click', (e)=>{let el = e.target;while(!el.matches('.child')){if(el === grandpaEl){el = null;break;}el = el.parentNode;}if(el){console.log('我是儿子,我被点击了!');}})
可以封装成一个事件委托(代理)函数如下:
function delegate(agent, eventType, clientSelector, fn){agent.addEventListener(eventType, (e)=>{let el = e.target;while(!el.matches(clientSelector)){if(el === agent){el === null;break;}el = el.parentNode;}el && fn.call(el, e, el);})return agent;}
3. 巧用事件对象的 path 属性
const grandpaEl = document.getElementsByClassName('grandpa')[0];grandpaEl.addEventListener('click', (e)=>{let child = e.path.find(el => el.matches('.child'));if(child){console.log('我是儿子,我被点击了!');}})
优缺点
优点:
- 节省监听器数量
- 可以实现动态监听,在元素还没出现的时候提前监听。
缺点:
- 调试困难,无法快速定位到当前元素的所有监听器。
