13

面试官提出的问题

我们在面试前端的过程中,经常会听到面试官问这样的问题:

如果我有一个页面,里面1000个元素都要绑定click事件,请问你要怎么做

如果你回答逐个绑定那估计可以直接回家了,面试官希望的答案是你来高谈阔论事件委托,你应该能给出方法并写出解决方案。
接下来,考官一定要问,这么做的好处是什么,或者你为什么用事件委托。

我认为好处主要有两个:

  • 事件只需要绑定一次,而不是绑定1000次,提高效率
  • 动态添加进父级的元素自动拥有事件

根据http://www.nczonline.net/blog/2009/06/30/event-delegation-in-javascript/ 更专业的说法如下:

  • Fewer functions to manage. 管理较少的函数
  • Takes up less memory. 更少的内存消耗
  • Fewer ties between your code and the DOM.降低代码和dom之间的关联
  • Don’t need to worry about removing event handlers when changing the DOM via修改dom的时候不用考虑删除事件。

这还没完,面试官通常会接下来问,那么,你绑定在父级上,页面怎么知道你点击的是哪一个?

我想这应该回答,利用了事件的冒泡机制。

等等,挺在这里,虽然不仅一篇文章阐述了事件委托是利用了冒泡机制,得益于冒泡机制,但是,怎么得益的,怎么利用的。

于是好奇的我引申出另外一个复杂的议题,事件的绑定和执行机制。

事件执行

很多文章里介绍了这个机制,这篇文章很简明详尽:
http://www.quirksmode.org/js/events_order.html

概括一下,就是由于历史原因,浏览器对事件的处理有两种模式,一种是先执行外面父级元素的事件(捕获模式 capturing),一种先执行内部元素的事件(冒泡模式bubbling)。这个概念就类似图层,一种是外面的在前面,另一种是里面在前面。

现代浏览器里参照w3c规范,采用了这两种方式并行的方式,简单的来说就是先捕获,再冒泡。(为什么,为什么,规定为一种不好么)

我们都知道为某个元素注册事件,是通过addEventListener这个方法,那么,我直接注册的事件,他是属于捕获执行呢,还是冒泡执行呢?还是说我捕获阶段执行一次,冒泡阶段再执行一次。难道说每次都执行两次?

不对,同样一次绑定的方法是执行一次,因为它要么属于捕获阶段,要么属于冒泡阶段。这两个阶段就像通向公司的两条路,你去公司是一条路,回来是另外一条路,你的事件是路上的小卖铺,它要么在去的那条路上,要么在回来的那条路上。当然你也可以两条路上都开一家小卖铺,虽说没什么意义,但是这样事件触发的时候确实执行了两次,不过这也能证明事件执行的两个阶段。

事件执行顺序的实验

先忘掉上面的理论,下面我们来做个试验,记住下面的html,我们打算为out同时绑定两个事件,看看执行顺序是怎么样的

<div id="out"> 
    <div id='inner1'>
       Click
    </div>
</div>
var a = document.querySelector('div#out')
var b = document.querySelector('div#inner1')

a.addEventListener('click',function(e){
   alert(e.target+'事件A')
})
a.addEventListener('click',function(e){
   alert(e.target+'事件B') 
})

我们可以看到,为同一个元素先后绑定两个事件,执行的顺序是从上到下的,把事件B提到A前面就会先执行B。

接下来我们试一下通过addEventListener第三个参数指定事件绑定在哪条路上,false为冒泡阶段,true为捕获阶段。

a.addEventListener('click',function(e){
    alert(e.target+'外部元素在冒泡阶段')
},false)

a.addEventListener('click',function(e){
    alert(e.target+'外部元素在捕获阶段')
},true)

这下我们可以看到,不论冒泡在前面还是后面,都是先执行捕获阶段的那行代码,佐证了先前说道的w3c规范下的先执行捕获再执行冒泡的行为。

不过在这里的实验中,我无意发现了一个有趣的现象,当你把html改成没有子元素,比如

<div id="out"> 
       Click
</div>

这时候就不遵循先捕获再冒泡的原则了,看起来像是判断节点没有子元素,就不需要使用捕获和冒泡的流程,只采用先来后到的顺序。这其中的原理还望高手指教。

总结: 当一个页面元素包含子元素节点的时候,他在处理在其身上的绑定事件的时候,采用先执行捕获阶段的事件,再执行冒泡阶段的事件。而事件处于哪个阶段,是由addevnetlistener的第三个参数决定的。

阻止冒泡

我们都知道,阻止冒泡是采用类似 stopPropagation()的方法。但是请考虑这样一个问题:

a.addEventListener('click',function(e){
    alert(e.target+'外部元素在冒泡阶段')
},false)
a.addEventListener('click',function(e){
    alert(e.target+'外部元素在捕获阶段')
},true)

b.addEventListener('click',function(e){
    e.stopPropagation()
    alert(e.target+'内部元素上的事件')
})

这段代码里,我点击b,事件触发的顺序是怎样的?
答案是:外部元素捕获 ---> 内部元素事件

因为捕获是永远优先执行的,内部元素由于不存在子元素,所以只有一个阶段,无所谓先执行后执行,由于自身没有冒泡事件,所以stoppropagation() 掠过自身,寻找父级的冒泡阶段上的事件,一次查找,全部给阻止掉。

所以,想要点击内部的时候无视外部事件,一定不要把外部的事件放在捕获阶段,就是说第三个参数不要设为true。

我们再把代码搞更复杂一些:

<div id="out">
    <div id="inner1">
      <div id="inner2">
        click 
      </div>
    </div>
</div>
var a = document.querySelector('div#out')
var b = document.querySelector('div#inner1')
var c = document.querySelector('div#inner2')

a.addEventListener('click',function(e){
    alert('a在冒泡阶段') 
},false)
a.addEventListener('click',function(e){
    alert('a在捕获阶段')
},true)

b.addEventListener('click',function(e){
    alert('b在冒泡阶段')
},false)
b.addEventListener('click',function(e){
    alert('b在捕获阶段')
},true)

c.addEventListener('click',function(e){
    alert(e.target+'内部元素事件')
})

执行一遍,可以加深了对事件这个模式的理解,顺序是这样的

a捕获 ---> b捕获 ---> 内部事件 ---> b冒泡 ---> a冒泡

这里我感兴趣的是阻止冒泡会怎么样,测下来是,如果把stoppropagation() 放在b,b本身的冒泡还是会执行,那么同理如果放在c,c本身如果有冒泡事件也会执行,

所以stoppropagation()所做的事情可以这么理解,阻止父级元素冒泡阶段的事件。

事件委托和冒泡机制有关系吗?

接下来我想引出本文的重点:事件委托和冒泡机制有关系吗?
我认为就算有关系,关系也不大。

我们先来看一下一个常见的事件委托例子:

// Get the element, add a click listener...
document.getElementById("parent-list").addEventListener("click",function(e) {
    // e.target is the clicked element!
    // If it was a list item
    if(e.target && e.target.nodeName == "LI") {
        // List item found!  Output the ID!
        console.log("List item ",e.target.id.replace("post-")," was clicked!");
    }
});

简言之,绑定在父类上一个事件,然后通过回调函数的参数获得当前点击的是哪一个元素,相当于把事件绑定在子元素身上。

请问这跟上文长篇累牍的冒泡机制有什么联系?

假设不存在冒泡或者捕获,在父类上点击到了子类或者不论点到哪,这个事件都是要执行的,子类这个元素还会作为引用传到函数体里,我实在看不出这个冒泡有什么关系,如果要什么导致事件委托可以实现,应当是函数体内的引用才是

那么,这个引用是什么情况呢,继续上面的实验:

var a = document.querySelector('div#out')
var b = document.querySelector('div#inner1')
var c = document.querySelector('div#inner2')

a.addEventListener('click',function(e){
    alert('a在冒泡阶段')
    console.log(e.target)//inner2
    console.log(this)//out
    console.log(e.currentTarget)//out
},false)
a.addEventListener('click',function(e){
    alert('a在捕获阶段')
    console.log(e.target)//inner2
},true)

b.addEventListener('click',function(e){
    alert('b在冒泡阶段')
    console.log(e.target)//inner2
},false)
b.addEventListener('click',function(e){
    alert('b在捕获阶段')
    console.log(e.target)//inner2
},true)

c.addEventListener('click',function(e){
    alert(e.target+'内部元素事件')
    console.log(e.target)//inner2
})

我们能看到,不论在哪一层里,e.target都是你当前点击的本身,这毫不奇怪,因为e本身是一个event对象,比如这里的MouseEvent,里面还带了是否同时按下alt键,鼠标位置等信息,可见这个对象本身可以说是和绑定主体无关了,和事件有关。所以,和冒泡还是没啥关系。以上代码里还展示了两种获取事件执行主体的方法,分别是e.currentTarge 和 this

所以我的观点是,虽然提到js的事件委托通常都会联系到冒泡,但是就算当初没有设计冒泡和捕获,事件委托还是事件委托,它依赖的是event对象传递到监听函数里面了,和其他无关。


fishenal
3.4k 声望159 粉丝

千山鸟飞绝万径人踪灭