站点工具

用户工具


当让你封装一个事件代理函数时

一天,学生小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

若愚 · 2023/02/08 19:05 · 当让你封装一个事件代理函数时.txt