一天,学生小A兴致勃勃的找我
A: 老师,看我封装了一个事件代理函数,看起来还行吧
<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) }) </Script>
若愚: 不错,代码能运行,不过有很多可以优化的地方。比如 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
自荐一下我的前端系统课,文章内容绝大多数都分散在课程的主线任务、项目、日常拓展课直播里
饥人谷一直致力于培养有灵魂的编程者,打造专业有爱的国内前端技术圈子。如造梦师一般帮助近千名不甘寂寞的追梦人把编程梦变为现实,他们以饥人谷为起点,足迹遍布包括facebook、阿里巴巴、百度、网易、京东、今日头条、大众美团、饿了么、ofo在内的国内外大小企业。 了解培训课程:加微信 xiedaimala03,官网:https://jirengu.com