# 当让你封装一个事件代理函数时
一天,学生小A兴致勃勃的找我
A: 老师,看我封装了一个事件代理函数,看起来还行吧
```html <body>
<div class="button">1</div> <div class="button">2</div> <div class="button">3</div> <div class="button">4</div> <div class="other">other</div>
</body> <script> function delegate(eventType, root, selector, handler) {
root.onclick = function(e) { if(e.target.classList.contains(selector)) { handler(e) } }
}
delegate('click', document.body, 'button', function(e) {
console.log('click button') console.log(e.target.innerText)
})
> 若愚: 不错,代码能运行,不过有很多可以优化的地方。比如 1. 如果button不是class,而是id,代码是不是又要改?而且第三个参数 'button' 让人感觉像传递的是标签名,而你这里明明需要传递的是class的名字。 过了几分钟 > A: 老师,看我改成这样,perfect!
function delegate(eventType, root, selector, handler) { root.onclick = function(e) {
if([...root.querySelectorAll(selector)].includes(e.target)) { handler(e) }
} }
delegate('click', document.body, '.button', function(e) { console.log('click button') console.log(e.target.innerText) })
> 若愚: 不错呀,基本功挺扎实,不过还有些问题,估计你已经发现了,看下面的场景,如果用户点到第一个按钮的span元素上,对用户来说点击的就是button,但实际上e.target是span导致你的判断失效
<body> <div class="button"><span>1</span></div> <div class="button"><span>2</span></div> <div class="button"><span>3</span></div> <div class="button">4</div> <div class="other">other</div> </body>
<script>
function delegate(eventType, root, selector, handler) {
root.onclick = function(e) {
if([...root.querySelectorAll(selector)].includes(e.target)) { handler(e) }
} }
delegate('click', document.body, '.button', function(e) {
console.log('click button')
console.log(e.target.innerText)
})
//点击button1 的
</script>
> A: 其实刚刚写demo的时候我就发现了,搞不定偷偷把button里面的span删除了,看来又被你发现了。我捋一捋:我们要给把用户点击button的事件监听代理到容器body上,当我点击button里面的内容本质上也是点击button,应该也要被监听到,而此时event.target是button里面的span 。 我知道了,事件会冒泡,我们可以根据event.target.parent 层层往上找,只要碰到一个父级元素符合selector的描述,就表示需要被监听到
<body> <div class="button"><span>1</span></div> <div class="button">2</div> <div class="button">3</div> <div class="button">4</div> <div class="other">other</div> </body>
<script>
function delegate(eventType, root, selector, handler) {
root.onclick = function(e) { let target = e.target while(target && target !== root) { if([...root.querySelectorAll(selector)].includes(target)) { handler(e) break } target = target.parentNode } }
}
delegate('click', document.body, '.button', function(e) {
console.log('click button') console.log(e.target.innerText)
})
</script>
> 若愚: 很棒呀,再提三个优化的点。 > 1\. delegate里用addEventListener而不是onclick 绑定事件,否则你多次执行delegate之前的设置就无效了。 > 2\. 你可以了解一下 event.composedPath ,这个是个数组,包含事件从目标到根节点经历的所有DOM对象,你可以根据selector元素在不在这个数组里来判断,这样就不用while循环了。 > 3\. handler函数里面的this也设置一下 > A:这个我之前我之前还真没注意到,稍等查一下API... 搞定了!
<body> <div class="button"><span>1</span></div> <div class="button"><span>2</span></div> <div class="button"><span>3</span></div> <div class="button"><span>4</span></div> <div class="other">other</div> </body>
<script>
function delegate(eventType, root, selector, handler) {
root.addEventListener(eventType, function(e) {
let path = e.path || e.composedPath() const element = [...this.querySelectorAll(selector)].find(el => path.includes(el)) if(element) handler.bind(element)(e)
}) }
delegate('click', document.body, '.button', function(e) { console.log('click button') console.log(e) console.log(this) console.log(this.innerText) })
delegate('click', document.body, '.other', function(e) { console.log('click other') console.log(e) console.log(this) })
</script>
> 非常棒, 不过需要注意这个 event.composedPath有些新,如果需要适配IE浏览器不要考虑用 [](https://link.zhihu.com/?target=http%3A//js.jirengu.com/gumaq/edit%3Fhtml%2Coutput) 自荐一下我的[前端系统课](https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/1svw7rUXLK4SYgGngEQuew),文章内容绝大多数都分散在课程的主线任务、项目、日常拓展课直播里 > 饥人谷一直致力于培养有灵魂的编程者,打造专业有爱的国内前端技术圈子。如造梦师一般帮助近千名不甘寂寞的追梦人把编程梦变为现实,他们以饥人谷为起点,足迹遍布包括facebook、阿里巴巴、百度、网易、京东、今日头条、大众美团、饿了么、ofo在内的国内外大小企业。 了解培训课程:加微信 [xiedaimala03](https://wiki.jirengu.com/lib/exe/fetch.php?w=400&tok=5c45ca&media=%E9%A5%A5%E4%BA%BA%E8%B0%B7%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1%E5%B0%8F%E5%8A%A9%E7%90%86black.png),官网:https://jirengu.com