Javascript捕捉(capturing)与冒泡(bubbling)的区别

  张一帆   2016年12月10日


  今天,当我在做nodejs的时候刚巧碰到了javascript的冒泡机制。曾经,我是有看过这方面的知识。但是,已经好久没有碰触javascript的内部原理了。所以,有些淡忘了。我又重新做了下研究并记录于此。

  我们知道一个网页是由DOM(文档对象模型)控制着结构的。在HTML中很多地方都是一个标签套着另一个标签。这样的结构渲染在页面上的时候你是发现不出层级关系的。当你在页面上操作一个元素的时候,这个元素很可能不是唯一一个在一个嵌套体内有监听事件的元素。也就是说在他的上级或者下级都有可能一直在监听着同一种事件,那这个时候谁先谁后的问题就产生了。

  在分析之前,我先做个例子。这个例子运用了javascript的addEventListener函数

  在addEventListener函数中的第三个参数是用来指定冒泡/捕捉类型的。这个参数对于整体的设置监听事件有很大的帮助。false是冒泡(默认),turetrue是捕捉。

两种模型

  • 捕获模型事件 (Event capturing)

    就是从DOM结构的外向里的方向进行响应事件。

  • 冒泡模型事件 (Event bubbling)

    就是从DOM结构的里向外的方向进行响应事件。

  以上是两种模型,然而不光这两个模型影响着事件谁先执行,浏览器自己也影响着。不同的浏览器有自己的核心,所以也有着自己独特的设计。其实无外乎三种,就是内往外,外向内,从外到内再从内到外。可以把这个理解为过程,比如冒泡就是将事件在这个过程中进行绑定,而并不是触发。我觉得这个有可能会有点绕,但是一旦理解了这个机制大概就能明白了。比如冒泡事件,就是将同样的事件绑定到了包含关系的两个dom结构中,优先发生在最里层的dom元素,然后再往外层寻找dom元素再进行发生。 我也是从别人家的博客看到的。可以看看底下参考链接中的第一篇文章,这篇文章是外国人写的后国人翻译成中文的,虽然里面有些翻译问题,但是你跟着他的思路想还是能够想的差不多的。

  现如今,各种浏览器也被强制要求采用统一标准了,这个标准就是W3C标准。当浏览器统一标准后兼容性问题就会变得越来越少。所以,我真的不想总去测试那些在IE6上的效果,我觉得对自己一点意义没有。

  在我们编写js的时候,经常会给元素添加onclick事件,这些事件默认的都是冒泡阶段保定事件的。所以,我们在开发的时候不用特别注意的去选择捕捉还是冒泡。但是,我们需要知道的是如何去阻止冒泡的行为。参看参考中的第三篇文章,这里面就给出了e.stopPropagation()的用法。

  在写这篇文章的时候,自己去写了一下例子。在测试的时候我发现一个问题,那就是addEventListener这个函数的第二个参数如果你写成这样:

  也就是我想通过调用函数时通过传递参数的形式传送到定义好的函数时,当刷新页面的时候页面会将alert输出出来。于是我就查了一下原因,找到了参考中的第五篇文章。看完作者的文章以后我有个疑惑,我觉得作者对js的理解并不是很深(虽然我也不是很深吧)。因为,我们知道js的匿名函数如果后面增加了一个括号的话,意思就是要立刻执行。那么这个我想这个立即执行的原因应该是跟js的编译机制有关系,js在对编码进行编译的时候会将这样的函数调用立即进行调用的。当我写到这的时候我去百度搜索了一下我印象中的几篇博客,但是没有找到我曾看到的那段话。下次吧,我等下次再看js编译顺序的文章时再过来进行补完吧,关键词大概就是javascript闭包之类的。

以下写于 2017/04/26

  今天,又看到了一篇w3c的文章Event order,然后回忆起自己曾经写过一篇类似的文章,就又重新找到了这里,突然发现当时写的有些简陋了,所以就想再做一些补充。

  让我们先看一个图:

-----------------------------------
| element1                        |
|   -------------------------     |
|   |element2               |     |
|   -------------------------     |
|                                 |
-----------------------------------

  如果两个元素都绑定了onclick事件,我在点击element2的时候,element1和element2都会响应我的onclick事件,那么哪个事件在先,哪个事件在后呢?让我们用个例子看看吧。

  我们,选择到Result标签,尝试着456以后,在控制台中可以看到console.log的结果。通过例子我们可以看到,点击456的时候,控制台就会先打出2再打出1。这个是因为,以当前点击的元素为最终节点,从window向当前点击的dom上层层推进,第一个遇到的是ele1发现他是冒泡模式,不触发而接着向里面寻找直到到达当前点击的元素。然后,按照冒泡模式进行发生,也就是先打印出2,再打印出1了。这样,我就可以给出两种事件模型的图来了:

  • 捕获事件
               | |
---------------| |-----------------
| element1     | |                |
|   -----------| |-----------     |
|   |element2  \ /          |     |
|   -------------------------     |
|        Event CAPTURING          |
-----------------------------------
  • 冒泡事件
               / \
---------------| |-----------------
| element1     | |                |
|   -----------| |-----------     |
|   |element2  | |          |     |
|   -------------------------     |
|        Event BUBBLING           |
-----------------------------------

  这和我最上面的两个类型相互呼应。

  那么问题来了,如此两种事件类型,我哪知道我用的浏览器是默认使用哪种发生事件的机制的呢?

  答案就是w3c来为我们明智的做出了解决方案。w3c模型采取了两者的结合。请看图:

  • w3c 模型
                 | |  / \
-----------------| |--| |-----------------
| element1       | |  | |                |
|   -------------| |--| |-----------     |
|   |element2    \ /  | |          |     |
|   --------------------------------     |
|        W3C event model                 |
------------------------------------------

  作为一个开发者,我们可以选择想要使用发生机制。我们可以通过 addEventListener() 的最后一个参数true/false来抉择我们想要的方式。如果选择true的话那么就是捕捉,如果选择false的话就是冒泡。默认为false。

  让我们来看看第二种情况:

  我们发现,ele1这个时候我设置的最后一个参数为true,也就是说这个是捕捉模式。按照之前的理论,js从window向所点击的dom寻找,第一个先触及ele1,发现他是使用的捕捉模式,直接发生onclick事件。

  有兴趣的同学,可以自己写个例子尝试一下

element1.addEventListener('click',doSomething2,true)
element2.addEventListener('click',doSomething,false)

element1.addEventListener('click',doSomething2,false)
element2.addEventListener('click',doSomething,false)

的区别吧。

  其实,现如今开发者不会将太多的注意力放在什么时候使用捕捉还是冒泡模式。当然也就不会关心那些事件类型可以用来冒泡了,他们更想的是我想实现哪个地方点击出效果,哪些地方不出效果,也就是不同事件彼此分开来。但是,这种想法在将来有可能会成真。然而,对于目前来说还不可能,所以我们还是需要闹清楚这些区别的。因为,在将来的某个时候也许你就会被这个问题弄得焦头烂额滴。

  为了暂时解决我就想触发我要点击的对象的click方法,不想触发上层的click事件。其实,也是有办法的。我们可以通过设置关闭冒泡功能来关闭掉冒泡事件。

  在微软浏览器中我们可以采用:

window.event.cancelBubble = true

  在W3C浏览器中我们可以采用:

e.stopPropagation()

  让我们来看一个例子吧:

  在这个例子中,我将ele1和ele2同时绑定了一个叫abc的方法。并且在abc中设置了

  if (!e) var e = window.event;
  e.cancelBubble = true;
  if (e.stopPropagation) e.stopPropagation();

  这三行就是说不论是在微软的浏览器中还是在w3c的浏览器中,我都关闭了冒泡事件。表现出来的效果就是,当我点击456的时候,他只会打印出一个1。也就是说ele1的abc方法没有触发。用这种办法,我们就可以实现我想点击哪个元素,那个元素就给我回应,其他的别回应我。

  那么现在又有问题了,这个abc方法,我哪知道到底是ele1回应给我的还是ele2回应给我的呢?

  我想答案肯定会让大家失望了。微软的浏览器根本不能实现这个功能,只能靠你自己的经验了。XD。但是,W3C的浏览器有个currentTarget。通过这货我们就可以知道喽,来看例子:

  当我们点击456的时候就会打印出:

<div id="ele2">456</div>

  当我们点击123的时候就会打印出:

<div id="ele1">
  123
  <div id="ele2">456</div>
</div>

  好了,到了这里我想我已经总结明白了关于javascript的捕捉和冒泡的全部知识要点了。下面就让我来说说上面之前写的时候留下的问题。

  通过刚刚我举得例子,带有abc方法的那两个。如果我们把代码改一下。如果是这样:

ele1.addEventListener('click',abc(),false);

  那么,一旦我们刷新页面就会调用abc方法了。然后,当我点击ele1的时候就不会再起作用了。具体为什么,当然是abc后面的那个()了。任何函数只要加了后面的(),代表的就是立即运行,一切与之相关的方法,他都不会参加。因为立刻运行以后方法就从内存中消失了,之后的addEventListener绑定给ele1的click事件指向的就是一个空指针了,所以就不好使了。具体原理我想应该是event loop的关系。我还没有太仔细看loop的相关知识,也许等我看完以后再来解释一下也不迟呢。

本文参考: