JS 中的事件设计

看懂此文,再也不困惑于 JS 中的事件设计javascript

抽空学习了下javascript和jquery的事件设计,收获颇大,总结此贴,和你们分享。html

(一)事件绑定的几种方式

javascript给DOM绑定事件处理函数总的来讲有2种方式:在html文档中绑定、在js代码中绑定。下面的方式一、方式2属于在html中绑定事件,方式三、方式4和方式5属于在js代码中绑定事件,其中方法5是最推荐的作法。

方式1:java

HTML的DOM元素支持onclick、onblur等以on开头属性,咱们能够直接在这些属性值中编写javascript代码。当点击div的时候,下面的代码会弹出div的ID:jquery

这种作法很显然很差,由于代码都是放在字符串里的,不能格式化和排版,当代码不少的时候很难看懂。这里有一点值得说明:onclick属性中的this表明的是当前被点击的DOM对象,因此咱们能够经过this.id获取DOM元素的id属性值。浏览器

方式2:dom

当代码比较多的时候,咱们能够在onclick等属性中指定函数名。函数

跟上面的作法相比,这种作法略好一些。值得一提的是:事件处理函数中的this表明的是window对象,因此咱们在onclick属性值中,经过this将dom对象做为参数传递。 post

 1 <script>
 2 
 3     function buttonHandler(thisDom)
 4     {
 5         alert(this.id);//undefined
 6         alert(thisDom.id);//outestA
 7         return false;
 8     }
 9 </script>
10 <div id="outestA" onclick="return buttonHandler(this);"></div>

方式3:在JS代码中经过dom元素的onclick等属性学习

1 var dom = document.getElementById("outestA");
2 dom.onclick = function(){alert("1=" + this.id);};
3 dom.onclick = function(){alert("2=" + this.id);};

这种作法this表明当前的DOM对象。还有一点:这种作法只能绑定一个事件处理函数,后面的会覆盖前面的。测试

方式4:IE下使用attachEvent/detachEvent函数进行事件绑定和取消。

attachEvent/detachEvent兼容性很差,IE6~IE11都支持该函数,可是FF和Chrome浏览器都不支持该方法。并且attachEvent/detachEvent不是W3C标准的作法,因此不推荐使用。在IE浏览器下,attachEvent有如下特色。

a) 事件处理函数中this表明的是window对象,不是dom对象。

1 var dom = document.getElementById("outestA");  
2 dom.attachEvent('onclick',a);  
3       
4 function a()  
5 {   
6     alert(this.id);//undefined  
7 }

b) 同一个事件处理函数只能绑定一次。 

1 var dom = document.getElementById("outestA");  
2 dom.attachEvent('onclick',a);  
3 dom.attachEvent('onclick',a);    
4 function a()  
5 {  
6     alert(this.id);
7 }

虽然使用attachEvent绑定了2次,可是函数a只会调用一次。

 
c)不一样的函数对象,能够重复绑定,不会覆盖。
1 var dom = document.getElementById("outestA");  
2 dom.attachEvent('onclick',function(){alert(1);});  
3 dom.attachEvent('onclick',function(){alert(1);});  
4 
5 // 当outestA的click事件发生时,会弹出2个对话框

匿名函数和匿名函数是互相不相同的,即便代码彻底同样。因此若是咱们想用detachEvent取消attachEvent绑定的事件处理函数,那么绑定事件的时候不能使用匿名函数,必需要将事件处事函数单独写成一个函数,不然没法取消。

方式5:使用W3C标准的addEventListener和removeEventListener。

这2个函数是W3C标准规定的,FF和Chrome浏览器都支持,IE6/IE7/IE8都不支持这2个函数。不过从IE9开始就支持了这2个标准的API。
1 // type:事件类型,不含"on",好比"click"、"mouseover"、"keydown";
2 // 而attachEvent的事件名称,含含"on",好比"onclick"、"onmouseover"、"onkeydown";
3 // listener:事件处理函数
4 // useCapture是事件冒泡,仍是事件捕获,默认false,表明事件冒泡类型
5 addEventListener(type, listener, useCapture);

a) 事件处理函数中this表明的是dom对象,不是window,这个特性与attachEvent不一样。

1 var dom = document.getElementById("outestA");  
2 dom.addEventListener('click', a, false);  
3       
4 function a()  
5 {   
6     alert(this.id);//outestA  
7 }

b) 同一个事件处理函数能够绑定2次,一次用于事件捕获,一次用于事件冒泡。

 1 var dom = document.getElementById("outestA");  
 2 dom.addEventListener('click', a, false);  
 3 dom.addEventListener('click', a, true);  
 4       
 5 function a()  
 6 {   
 7     alert(this.id);//outestA  
 8 }
 9 
10 // 当点击outestA的时候,函数a会调用2次

若是绑定的是同一个事件处理函数,而且都是事件冒泡类型或者事件捕获类型,那么只能绑定一次。

 1 var dom = document.getElementById("outestA");  
 2 dom.addEventListener('click', a, false);  
 3 dom.addEventListener('click', a, false);  
 4       
 5 function a()  
 6 {   
 7     alert(this.id);//outestA  
 8 }
 9 
10 // 当点击outestA的时候,函数a只会调用1次

 c) 不一样的事件处理函数能够重复绑定,这个特性与attachEvent一致。

(二)事件处理函数的执行顺序

方式一、方式2和方式3都不能实现事件的重复绑定,因此天然也就不存在执行顺序的问题。方式4和方式5能够重复绑定特性,因此须要了解下执行顺序的问题。若是你写出依赖于执行顺序的代码,能够判定你的设计存在问题。因此下面的顺序问题,仅做为兴趣探讨,没有什么实际意义。直接上结论:addEventListener和attachEvent表现一致,若是给同一个事件绑定多个处理函数,先绑定的先执行。下面的代码我在IE十一、FF17和Chrome39都测试过。

 1 <script>
 2     window.onload = function(){
 3     <span style="white-space:pre">    </span>var outA = document.getElementById("outA");  
 4         outA.addEventListener('click',function(){alert(1);},false);
 5         outA.addEventListener('click',function(){alert(2);},true);
 6         outA.addEventListener('click',function(){alert(3);},true);
 7         outA.addEventListener('click',function(){alert(4);},true);
 8     };
 9 </script>
10 
11 <body>
12     <div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;">
13     </div>
14 </body>

 当点击outA的时候,会依次打印出一、二、三、4。这里特别须要注意:咱们给outA绑定了多个onclick事件处理函数,也是直接点击outA触发的事件,因此不涉及事件冒泡和事件捕获的问题,即addEventListener的第三个参数在这种场景下,没有什么用处。若是是经过事件冒泡或者是事件捕获触发outA的click事件,那么函数的执行顺序会有变化

(三) 事件冒泡和事件捕获

事件冒泡和事件捕获很好理解,只不过是对同一件事情的不一样见解,只不过这2种见解都颇有道理。
咱们知道HTML中的元素是能够嵌套的,造成相似于树的层次关系。好比下面的代码:
1 <div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;">
2     <div id="outB" style="height:200; background:#0000ff;top:100px;position:relative;">
3         <div id="outC" style="height:100px; background:#FFB90F;top:50px;position:relative;"></div> 
4     </div>
5 </div>

若是点击了最内侧的outC,那么外侧的outB和outC算不算被点击了呢?很显然算,否则就没有必要区分事件冒泡和事件捕获了,这一点各个浏览器厂家也没有什么疑义。假如outA、outB、outC都注册了click类型事件处理函数,当点击outC的时候,触发顺序是A–>B–>C,仍是C–>B–>A呢?若是浏览器采用的是事件冒泡,那么触发顺序是C–>B–>A,由内而外,像气泡同样,从水底浮向水面;若是采用的是事件捕获,那么触发顺序是A–>B–>C,从上到下,像石头同样,从水面落入水底。

事件冒泡见下图:

 

事件捕获见下图:

通常来讲事件冒泡机制,用的更多一些,因此在IE8以及以前,IE只支持事件冒泡。IE9+/FF/Chrome这2种模型都支持,能够经过addEventListener((type, listener, useCapture)的useCapture来设定,useCapture=false表明着事件冒泡,useCapture=true表明着采用事件捕获。

<script>

    window.onload = function(){
        var outA = document.getElementById("outA");  
        var outB = document.getElementById("outB");  
        var outC = document.getElementById("outC");  
        
        // 使用事件冒泡
        outA.addEventListener('click',function(){alert(1);},false);
        outB.addEventListener('click',function(){alert(2);},false);
        outC.addEventListener('click',function(){alert(3);},false);
    };
 
</script>

<body>
    <div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;">
        <div id="outB" style="height:200; background:#0000ff;top:100px;position:relative;">
            <div id="outC" style="height:100px; background:#FFB90F;top:50px;position:relative;"></div> 
        </div>
    </div>
</body>

使用的是事件冒泡,当点击outC的时候,打印顺序是3–>2–>1。若是将false改为true使用事件捕获,打印顺序是1–>2–>3。

(四) DOM事件流

DOM事件流我也不知道怎么解释,我的感受就是事件冒泡和事件捕获的结合体,直接看图吧。

 

DOM事件流:将事件分为三个阶段:捕获阶段、目标阶段、冒泡阶段。先调用捕获阶段的处理函数,其次调用目标阶段的处理函数,最后调用冒泡阶段的处理函数。这个过程很相似于Struts2框中的action和Interceptor。当发出一个URL请求的时候,先调用前置拦截器,其次调用action,最后调用后置拦截器。

<script>

    window.onload = function(){
        var outA = document.getElementById("outA");  
        var outB = document.getElementById("outB");  
        var outC = document.getElementById("outC");  
        
        // 目标(自身触发事件,是冒泡仍是捕获无所谓)
        outC.addEventListener('click',function(){alert("target");},true);
        
        // 事件冒泡
        outA.addEventListener('click',function(){alert("bubble1");},false);
        outB.addEventListener('click',function(){alert("bubble2");},false);
        
        // 事件捕获
        outA.addEventListener('click',function(){alert("capture1");},true);
        outB.addEventListener('click',function(){alert("capture2");},true);    
    };
 
</script>

<body>
    <div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;">
        <div id="outB" style="height:200; background:#0000ff;top:100px;position:relative;">
            <div id="outC" style="height:100px; background:#FFB90F;top:50px;position:relative;"></div> 
        </div>
    </div>
</body>

 当点击outC的时候,依次打印出capture1–>capture2–>target–>bubble2–>bubble1。到这里是否是能够理解addEventListener(type,handler,useCapture)这个API中第三个参数useCapture的含义呢?useCapture=false意味着:将事件处理函数加入到冒泡阶段,在冒泡阶段会被调用;useCapture=true意味着:将事件处理函数加入到捕获阶段,在捕获阶段会被调用。从DOM事件流模型能够看出,捕获阶段的事件处理函数,必定比冒泡阶段的事件处理函数先执行。

(五) 再谈事件函数执行前后顺序

在DOM事件流中提到过:

// 目标(自身触发事件,是冒泡仍是捕获无所谓)
outC.addEventListener('click',function(){alert("target");},true);

咱们在outC上触发onclick事件(这个是目标对象),若是咱们在outC上同时绑定捕获阶段/冒泡阶段事件处理函数会怎么样呢?

 1 <script>
 2 
 3     window.onload = function(){
 4         var outA = document.getElementById("outA");  
 5         var outB = document.getElementById("outB");  
 6         var outC = document.getElementById("outC");  
 7         
 8         // 目标(自身触发事件,是冒泡仍是捕获无所谓)
 9         outC.addEventListener('click',function(){alert("target2");},true);
10         outC.addEventListener('click',function(){alert("target1");},true);
11         
12         // 事件冒泡
13         outA.addEventListener('click',function(){alert("bubble1");},false);
14         outB.addEventListener('click',function(){alert("bubble2");},false);
15         
16         // 事件捕获
17         outA.addEventListener('click',function(){alert("capture1");},true);
18         outB.addEventListener('click',function(){alert("capture2");},true);
19 
20         
21         
22     };
23  
24 </script>
25 
26 <body>
27     <div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;">
28         <div id="outB" style="height:200; background:#0000ff;top:100px;position:relative;">
29             <div id="outC" style="height:100px; background:#FFB90F;top:50px;position:relative;"></div> 
30         </div>
31     </div>
32 </body>

点击outC的时候,打印顺序是:capture1–>capture2–>target2–>target1–>bubble2–>bubble1。因为outC是咱们触发事件的目标对象,在outC上注册的事件处理函数,属于DOM事件流中的目标阶段。目标阶段函数的执行顺序:先注册的先执行,后注册的后执行。这就是上面咱们说的,在目标对象上绑定的函数是采用捕获,仍是采用冒泡,都没有什么关系,由于冒泡和捕获只是对父元素上的函数执行顺序有影响,对本身没有什么影响。若是不信,能够将下面的代码放进去验证。

1 // 目标(自身触发事件,是冒泡仍是捕获无所谓)
2 outC.addEventListener('click',function(){alert("target1");},false);
3 outC.addEventListener('click',function(){alert("target2");},true);
4 outC.addEventListener('click',function(){alert("target3");},true);
5 outC.addEventListener('click',function(){alert("target4");},false);

至此咱们能够给出事件函数执行顺序的结论了:捕获阶段的处理函数最早执行,其次是目标阶段的处理函数,最后是冒泡阶段的处理函数。目标阶段的处理函数,先注册的先执行,后注册的后执行

(六) 阻止事件冒泡和捕获

默认状况下,多个事件处理函数会按照DOM事件流模型中的顺序执行。若是子元素上发生某个事件,不须要执行父元素上注册的事件处理函数,那么咱们能够中止捕获和冒泡,避免没有意义的函数调用。前面提到的5种事件绑定方式,均可以实现阻止事件的传播。因为第5种方式,是最推荐的作法。因此咱们基于第5种方式,看看如何阻止事件的传播行为。IE8以及之前能够经过 window.event.cancelBubble=true阻止事件的继续传播;IE9+/FF/Chrome经过event.stopPropagation()阻止事件的继续传播。

 1 <script>
 2 
 3     window.onload = function(){
 4         var outA = document.getElementById("outA");  
 5         var outB = document.getElementById("outB");  
 6         var outC = document.getElementById("outC");  
 7         
 8         // 目标
 9         outC.addEventListener('click',function(event){
10             alert("target");
11             event.stopPropagation();
12         },false);
13 
14         // 事件冒泡
15         outA.addEventListener('click',function(){alert("bubble");},false);
16 
17         // 事件捕获
18         outA.addEventListener('click',function(){alert("capture");},true);        
19         
20     };
21  
22 </script>
23 
24 <body>
25     <div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;">
26         <div id="outB" style="height:200; background:#0000ff;top:100px;position:relative;">
27             <div id="outC" style="height:100px; background:#FFB90F;top:50px;position:relative;"></div> 
28         </div>
29     </div>
30 </body>

当点击outC的时候,以后打印出capture–>target,不会打印出bubble。由于当事件传播到outC上的处理函数时,经过stopPropagation阻止了事件的继续传播,因此不会继续传播到冒泡阶段。

最后再看一段更有意思的代码:

 1 <script>
 2 
 3     window.onload = function(){
 4         var outA = document.getElementById("outA");  
 5         var outB = document.getElementById("outB");  
 6         var outC = document.getElementById("outC");  
 7         
 8         // 目标
 9         outC.addEventListener('click',function(event){alert("target");},false);
10 
11         // 事件冒泡
12         outA.addEventListener('click',function(){alert("bubble");},false);
13 
14         // 事件捕获
15         outA.addEventListener('click',function(){alert("capture");event.stopPropagation();},true);        
16         
17     };
18  
19 </script>
20 
21 <body>
22     <div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;">
23         <div id="outB" style="height:200; background:#0000ff;top:100px;position:relative;">
24             <div id="outC" style="height:100px; background:#FFB90F;top:50px;position:relative;"></div> 
25         </div>
26     </div>
27 </body>

执行结果是只打印capture,不会打印target和bubble。神奇吧,咱们点击了outC,可是却没有触发outC上的事件处理函数,而是触发了outA上的事件处理函数。缘由不作解释,若是你还不明白,能够再读一遍本文章。

相关文章
相关标签/搜索