[关闭]
@xinx1n 2017-04-23T12:12:26.000000Z 字数 3849 阅读 2344

使用原生 JS 实现事件委托

JavaScript


什么是事件委托?我们需要对很多元素添加事件的时候,可以通过将事件添加到它们的父节点而将事件委托给父节点来触发处理函数。
事件委托本质上来说是一种优化事件绑定的方法,那么我们首先要了解一下事件的绑定以及事件触发以后的各个流程。

准备一点知识

事件绑定(监听)

通过onclick属性绑定事件

DOM Level 0 Event 从 Netscape 浏览器开始就有支持,最初是通过在 HTML 中写入 onclick 属性来完成事件绑定:

  1. <button onclick="clickHandler()" id=btn></button>
  2. <script>
  3. function clickHandler(){
  4. console.log('clicked')
  5. }
  6. </script>

随着前端技术的发展,在 JavaScript 中绑定事件成为可能,这种绑定方式我们现在还是经常用到:

  1. document.getElementById("#btn").onclick = function(){
  2. console.log('clicked')
  3. }

但是这种绑定方式也还有一些缺陷,通过分析上边的代码我们可以很轻易的发现,上述代码是一个给 HTML 元素的属性赋值赋值的过程,那么当 onclick层被赋值过以后,我们再次赋值就会覆盖掉原来的值,即每个元素的每一种事件我们只能绑定一次。

  1. var mbtn = document.getElementById("#btn")
  2. mbtn.onclick = function(){
  3. console.log('clicked0')
  4. }
  5. mbtn.onclick = function(){
  6. console.log('clicked1')
  7. }
  8. //当 button 被点击的时候,控制台会打出 clicked1,前边绑定的函数被覆盖了

通过addEventListener方法绑定事件

Document Object Model (DOM) Level 2 开始,W3C 给我们提供了更加复杂的事件模型,其中一种调用方式如下:
element.addEventListener("click", Listener, false)
前两个参数分别代表监听事件的类型和监听器(比较详细的使用说明见 MDN),而第三个参数的控制监听器在事件的哪个阶段触发,默认是 false,冒泡阶段,由此可见事件监听器有两种类型,分别监听不同的阶段。什么是冒泡阶段,还有什么阶段,我们下一段接着讲。

事件触发后的阶段

当事件触发后会经历三个阶段捕获阶段(capture phase)、目标阶段(target phase)、冒泡阶段(bubbling phase),所以上一节提到的两种监听器分别为 捕获监听器和冒泡监听器,关于这三个阶段W3C 的文档是这么描述的:

这三个阶段概括起来就是:
当事件触发(比如 click)
1. 事件首先从最上层元素传播到最下层元素(点击的 target 元素),这个过程路过的所有元素中如果绑定了捕获监听器,就会被触发。这一阶段就是捕获阶段。
2. 然后事件传播到 target 元素,target 元素上的两种监听器均会被触发。需要说明的是:触发顺序为先绑定先触发,后绑定后触发,而不是先触发捕获监听器后触发冒泡监听器。这个阶段就是目标阶段。
3. 最后,事件从最下层向上传递,可以看做是捕获阶段的逆过程,这一过程中经过的元素的冒泡监听器会被触发。这一阶段就是冒泡阶段。
下边放一张流传很广的图:

再放一张简洁点的:( ̄_ ̄|||)

了解了这三个阶段以后我们可以发现一个特性:
当我们给一个元素添加事件监听(无论捕获还是冒泡,默认使用冒泡)以后,无论事件发生在这个元素本身还是他的后代元素,事件监听器均会被启动,通过这一点我们就可以比较方便的实现事件委托。

开始动手

实现事件委托

当我们了解完事件触发后的几个阶段后,我们对实现事件委托也有了一个比较清晰的思路,当我们想要给许多元素(这里我们暂时管他们叫 eles )添加事件监听的时候,我们可以:
1. 给父元素添加事件监听器 parent.addEventListener("click", func)
2. 在函数 func 中,从事件触发的元素开始一层一层往上寻找,如果找到了 eles 就 do something,如果一直找到 parent 也没找到那就什么也不做。
根据这一思路我们就可以开始写代码了。

  1. parent.addEventListener('click',function (e) {
  2. let el = e.target
  3. while(el.tagName!=='BUTTON'){
  4. if(el===e.currentTarget){
  5. el = null
  6. break;
  7. }
  8. el = el.parentNode
  9. }
  10. el && console.log('do something')
  11. })

以上代码就实现了把 parent 内的 button 的 click 事件委托给了 parent,只有点击 parent 内的 button 时控制台才会打出 do something
我们把它抽取出来,写成一个函数,方便调用:

  1. function eventDelegation (element, eventType, selector, func) {
  2. element.addEventListener(eventType, function(e) {
  3. let el = e.target
  4. while (!el.matches(selector)) {
  5. if (el === element) {
  6. el = null
  7. break;
  8. }
  9. el = el.parentNode
  10. }
  11. el && func.call(el, el, e)
  12. })
  13. return element
  14. }
  15. // 调用
  16. let parentEl = document.getElementById('parent')
  17. eventDelegation(parentEl,'click','button',(element,e)=>{
  18. console.log(element,e)
  19. })

事件委托的好处

我们费半天劲实现事件委托是为了什么?当我们想给很多button绑定事件,这样写就行了:

  1. var btns = document.querySelectorAll('button')
  2. Array.prototype.forEach.call(btns, function(el) {
  3. el.addEventListener('click', function() {
  4. console.log('click-button')
  5. })
  6. })

以上代码和用事件委托有什么区别?

总得有个尾巴

凭着感觉一路写下来,一些错误难以避免,如果有人发现的话可以直接在网页上标注出来,谢谢啦。
准备洗洗睡了……

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注