添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

对于如何给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按照冒泡的方式由内而外依次展示:

下边的例子,是对子元素使用了relativeposition样式,使其不再覆盖其父元素,而覆盖了其它元素,看起来像是第二个元素的子元素:
[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

在上边的例子里,对于每个元素都同时以冒泡和捕获方式绑定了事件处理方法,并且先为冒泡方式进行绑定,再以捕获方式绑定。当点击发生后,我们可以看到输出结果:

从结果上可以看出,父元素的事件处理方法都按捕获和冒泡都有序地正确执行,而在目标阶段,目标元素自身的冒泡方式却先于捕获方式执行。可以认为这是按照绑定顺序进行的,但是这不足以保证

隐藏的元素

接下来,再看一下在页面上看不见的元素对事件的“态度”。如何让元素看不见呢?最常用的两个样式属性就是 ;;了。前者会保留元素原来的位置,后者不会。对于后者,我们连点击的位置都没有,所以可以说直接忽略事件。那么前者呢?点击它原来的位置是否还有效?

修改前边第二个例子,给第一个蓝色元素添加一个;的样式:

[codesyntax lang=”xml”]

<samp name="dom-event-order-sample" id="sample-relative-hidden">
    <span id="relative-hidden-1" name="sample-relative-hidden" style=";">
        <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]

Click Me

之前我们点击第二个蓝色元素的时候,总是被红色部分拦截,而现在则可以正常点击,而第一个元素虽然还有占位,但是不会响应点击事件了。

除了使用上边两个方法让元素在页面上被隐藏,其实还有另一种方法可以它消失,那就是设置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;只是视觉不可见,而逻辑依然可以

References:
  • EventTarget – Document Object Model (DOM) | MDN
  • Document Object Model (DOM) Level 3 Events Specification – W3C
  • WPF Tutorial | Logical- and Visual Tree
  • Javascript – Event order
  • Bubbling and capturing | JavaScript Tutorial
  • What’s the difference between event.stopPropagation and event.preventDefault? – Stack Overflow

    Leave a Reply Cancel reply

    Your email address will not be published.

  •