对于如何给DOM绑定JavaScript的事件,在之前的文章已经比较详细的介绍了三种方式的区别和优缺点。不过,仍然还有一些遗留问题没有详细讲述,比如addEventListener的第三个参数useCapture有何神奇之处;假如HTML元素的自身和其父结点都绑定了相同类型的事件, 这些事件的激发过程又是如何处理的呢。
每一个HTML页面,在被浏览器解析后,都会生成一个树形结构,而每一个结点都是一个对应于标签(Tag)的DOM(Document Object Model)实例。树形数据结构上,其深度体现着父子关系(parent/child),其广度表现为兄弟关系(sibling)。这些DOM实例首先是用作呈现页面,在页面呈现上(若不指定特殊的布局样式,如z-index、relative position、float):子结点会被父结点包裹,但是父结点会被子结点全部或部分覆盖,而同层的结点则按从上往下,从左往右的顺序依次排布——(这只是对HTML页面呈现的简单布局流描述,其实根据样式的不同,过程更复杂)。另一方面,它们还是事件目标,用来接收页面上触发的事件。
当鼠标事件、键盘事件等在页面上产生时,浏览器引擎会根据触发点找到位置上(包括页面位置以及z-index
)最接近的HTML元素(至于时怎么找到的,本文不做讨论分析,只介绍当DOM接受到事件后发生的事情),然后把事件派发给对应的DOM实例。之前介绍,子元素是覆盖在父元素之上的,所以也处在事件触发地理应也“受到牵连”。
冒泡式
大部分事件处理机制,比如Asp.NET、iOS、Android,也包括JavaScript,都会实现冒泡方式的事件传递。即在某个结点接受到事件后,它会将该事件传递给其父结点。只是在传递的条件上有所不同,比如iOS只在当前元素无法处理该事件时才传递,而Android则根据事件处理方法返回的布尔值来判定是否传递给父元素。DOM实例无论是否能处理该事件,都会传递给其父结点,除非调用事件的stopPropagation方法来显示停止事件的“传播蔓延”。
冒泡方式的主要特点在于:子结点的事件处理方法会先于父结点的事件调用。用内嵌式、属性式,以及默认(第三个参数为false)的方法式绑定的事件都是以冒泡方式传播事件的。下边举几个例子来说明这种方式特点。所有的例子都会用下边的代码来绑定一个相同的事件,该事件把元素的id追加在一个变量上:
[codesyntax lang=”javascript”]
var eventOrderTestText = ''; function bindEventFor(spans, useCapture) { useCapture = !!useCapture; //false as default for (var i = 0; i < spans.length; i++) { var d = spans[i]; d.addEventListener('click', function(e) { eventOrderTestText += this.id + '(' + (useCapture?'capture':'bubble') + ')\n'; }, useCapture); } }
[/codesyntax]
此外,所有的例子都包裹在<samp name=”dom-event-order-sample”></samp>的标签内,下边的代码会对该标签绑定一个事件来alert()
变量内容并清空为下次做准备,因为事件时以冒泡式绑定,所以总是被最后一个调用到:
[codesyntax lang=”javascript”]
window.addEventListener('load', function() { var eventOrderSamps = document.getElementsByName('dom-event-order-sample'); for (var i = 0; i < eventOrderSamps.length; i++) { var samp = eventOrderSamps[i]; samp.onclick = function() { if (eventOrderTestText.length > 0) { alert(eventOrderTestText); eventOrderTestText = ''; } } } });
[/codesyntax]
先来看元素覆盖的情况:
[codesyntax lang=”xml”]
<samp name="dom-event-order-sample" id="sample-cover"> <span id="cover-1" name="sample-cover"> <span id="cover-1-1" name="sample-cover"> <span id="cover-1-1-1" name="sample-cover"> Click Me </span> </span> </span> </samp>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> bindEventFor(document.getElementsByName('sample-cover')); </script>
[/codesyntax]
Click Me
在点击之后,我们会看到元素的id按照冒泡的方式由内而外依次展示:
下边的例子,是对子元素使用了relative
的position
样式,使其不再覆盖其父元素,而覆盖了其它元素,看起来像是第二个元素的子元素:
[codesyntax lang=”xml”]
<samp name="dom-event-order-sample" id="sample-relative-position"> <span id="relative-position-1" name="sample-relative-position"> <span id="relative-position-1-1" name="sample-relative-position" style="background-color:#dd4b39; position:relative; left:110%;"> Click Me </span> </span> <span id="relative-position-2" name="sample-relative-position" > </span> </samp>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> bindEventFor(document.getElementsByName('sample-relative-position')); </script>
[/codesyntax]
Click Me
但是点击产生的效果,依然是传递给其真是的父结点,而被覆盖的元素则无法接受到红色部分传递点击事件过来。
对于这种现象,可以解释为逻辑树(Logical Tree)和视觉树(Visual Tree)上的不同,逻辑树规定了每个结点之间基本的关系,而视觉树则根据一些样式(Style)、变形(Transformation)来改变它的呈现效果。但是事件的激发顺序总是沿着逻辑树来传递。
这一点在Android的View动画(Animation)效果上就有体现:当一个按钮添加了Animation对象产生了位置的偏移,但是它的点击点依然在原来的地方。
捕获式
从子元素传递给父元素的冒泡式很常用的事件传递方式,同样反过来从父元素把事件传递给子元素也是可行,而这就是捕获式:明明传给子元素的事件,却先被父元素截取先处理了。一图顶万言,来看看DOM Level 3 Events Specification上对事件流的描绘:
对于之前第一个例子,我们简单将事件以捕获的方式绑定,就会发现最后id输出顺利颠倒了:
[codesyntax lang=”xml”]
<samp name="dom-event-order-sample" id="sample-capture"> <span id="capture-1" name="sample-capture"> <span id="capture-1-1" name="sample-capture"> <span id="capture-1-1-1" name="sample-capture"> Click Me </span> </span> </span> </samp>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> bindEventFor(document.getElementsByName('sample-capture'), true); </script>
[/codesyntax]
Click Me
捕获式只是说事件在接收者和其父结点之间相应的方式改变了,并不是事件激活时寻找激活点的顺序改变成从里往外。所以上边第二个例子,即使改成捕获式,点击红色区域也不会激活被覆盖元素的事件处理方法,而是先激发左侧蓝色元素,再激发红色元素的处理方法。
个人感觉对于捕获式,虽然只是传递顺序的改变,但是真正的实现这种方式并不那么简单。首先当事件方式的时候,我们总是先要找到响应该事件的最终端(这可以利用类似OpenGL的z buffering来检测)。在找到激活点元素之后,通过该元素找其父元素是简单的,应该每个结点只有一个父结点。通过冒泡方式,从端点到根点只要遍历一遍就可以把事件传播开去,还不需要任何额外的标记。而捕获式,我们可能需要先从端点到根点,并标记每一个路线,然后再从根点回到端点,这样我们就需要遍历两遍路径,并且还需要额外的标示。这可能就是为什么冒泡方式是各个事件处理的首先以及默认形式把。而且我也只在DOM事件机制中看到有捕获式的使用。应该各个浏览器引擎对此有更好的实现方式。
在上图中,我们也看到了整个事件传递过程有三个阶段:捕获阶段,目标阶段,冒泡阶段。从顺序上讲,捕获式绑定的事件处理方法要优先于冒泡式绑定的事件处理方法,不过这只是针对元素作为父结点的时候,当元素是激活目标时,各种方式绑定的事件处理方法被执行顺序不确定的。
[codesyntax lang=”xml”]
<samp name="dom-event-order-sample" id="sample-combine"> <span id="combine-1" name="sample-combine"> <span id="combine-1-1" name="sample-combine"> <span id="combine-1-1-1" name="sample-combine"> Click Me </span> </span> </span> </samp>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> bindEventFor(document.getElementsByName('sample-combine'), false); bindEventFor(document.getElementsByName('sample-combine'), true); </script>
[/codesyntax]
Click Me
在上边的例子里,对于每个元素都同时以冒泡和捕获方式绑定了事件处理方法,并且先为冒泡方式进行绑定,再以捕获方式绑定。当点击发生后,我们可以看到输出结果:
从结果上可以看出,父元素的事件处理方法都按捕获和冒泡都有序地正确执行,而在目标阶段,目标元素自身的冒泡方式却先于捕获方式执行。可以认为这是按照绑定顺序进行的,但是这不足以保证。
隐藏的元素
接下来,再看一下在页面上看不见的元素对事件的“态度”。如何让元素看不见呢?最常用的两个样式属性就是 visibility:hidden;
和display:none;
了。前者会保留元素原来的位置,后者不会。对于后者,我们连点击的位置都没有,所以可以说直接忽略事件。那么前者呢?点击它原来的位置是否还有效?
修改前边第二个例子,给第一个蓝色元素添加一个visibility:hidden;
的样式:
[codesyntax lang=”xml”]
<samp name="dom-event-order-sample" id="sample-relative-hidden"> <span id="relative-hidden-1" name="sample-relative-hidden" style="visibility:hidden;"> <span id="relative-hidden-1-1" name="sample-relative-hidden" style="background-color:#dd4b39; position:relative; left:110%;"> Click Me </span> </span> <span id="relative-hidden-2" name="sample-relative-hidden" ></span> </samp>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> bindEventFor(document.getElementsByName('sample-relative-hidden')); </script>
[/codesyntax]
之前我们点击第二个蓝色元素的时候,总是被红色部分拦截,而现在则可以正常点击,而第一个元素虽然还有占位,但是不会响应点击事件了。
除了使用上边两个方法让元素在页面上被隐藏,其实还有另一种方法可以它消失,那就是设置opacity
值为0让元素变成透明。看起来的效果跟visible:hidden;
是一样的。这里要注意的是opacity:0;
和颜色的alpha
值为0是不同的,前者会让其自身和子元素都变成透明不可见,后者只是说设置的颜色是透明的,不会影响到其子元素的内容设置和呈现。
[codesyntax lang=”xml”]
<samp name="dom-event-order-sample" id="sample-relative-transparent"> <span id="relative-transparent-1" name="sample-relative-transparent" style="opacity:0; filter:alpha(opacity=0); "> <span id="relative-transparent-1-1" name="sample-relative-transparent" style="background-color:#dd4b39; position:relative; left:110%;"> Click Me </span> </span> <span id="relative-transparent-2" name="sample-relative-transparent" > </span> </samp>
[/codesyntax][codesyntax lang=”javascript”]
<script type="text/javascript"> bindEventFor(document.getElementsByName('sample-relative-transparent')); </script>
[/codesyntax]
Click Me
此时我们再点击蓝色区域,我们依然会得到与第二个例子一样的结果:
这相当于是说opacity:0;
只是视觉不可见,而逻辑依然可以
有用,收藏,下次再来!