看懂此文,不再困惑於javascript中的事件綁定、事件冒泡、事件捕獲和事件執行順序

轉載自:http://blog.csdn.net/aitangyong/article/details/43231111


抽空學習了下javascript和jquery的事件設計,收穫頗大,總結此貼,和大家分享。

 

(一)事件綁定的幾種方式


javascript給DOM綁定事件處理函數總的來說有2種方式:在html文檔中綁定、在js代碼中綁定。下面的方式1、方式2屬於在html中綁定事件,方式3、方式4和方式5屬於在js代碼中綁定事件,其中方法5是最推薦的做法。

方式1:

HTML的DOM元素支持onclick、onblur等以on開頭屬性,我們可以直接在這些屬性值中編寫javascript代碼。當點擊div的時候,下面的代碼會彈出div的ID:

[html] view plain
  1. <div id="outestA" onclick="var id = this.id;alert(id);return false;"></div>  

這種做法很顯然不好,因爲代碼都是放在字符串裏的,不能格式化和排版,當代碼很多的時候很難看懂。這裏有一點值得說明:onclick屬性中的this代表的是當前被點擊的DOM對象,所以我們可以通過this.id獲取DOM元素的id屬性值。


方式2:

當代碼比較多的時候,我們可以在onclick等屬性中指定函數名。

[html] view plain
  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>  

跟上面的做法相比,這種做法略好一些。值得一提的是:事件處理函數中的this代表的是window對象,所以我們在onclick屬性值中,通過this將dom對象作爲參數傳遞。

 

方式3:在JS代碼中通過dom元素的onclick等屬性

[javascript] view plain
  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對象。

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

b) 同一個事件處理函數只能綁定一次。
[javascript] view plain
  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)不同的函數對象,可以重複綁定,不會覆蓋。
[javascript] view plain
  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。
[javascript] view plain
  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不同。
[javascript] view plain
  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次,一次用於事件捕獲,一次用於事件冒泡。
[javascript] view plain
  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次  

如果綁定的是同一個事件處理函數,並且都是事件冒泡類型或者事件捕獲類型,那麼只能綁定一次。
[javascript] view plain
  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一致。

(二)事件處理函數的執行順序


方式1、方式2和方式3都不能實現事件的重複綁定,所以自然也就不存在執行順序的問題。方式4和方式5可以重複綁定特性,所以需要了解下執行順序的問題。如果你寫出依賴於執行順序的代碼,可以斷定你的設計存在問題。所以下面的順序問題,僅作爲興趣探討,沒有什麼實際意義。直接上結論:addEventListener和attachEvent表現一致,如果給同一個事件綁定多個處理函數,先綁定的先執行。下面的代碼我在IE11、FF17和Chrome39都測試過。
[html] view plain
  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的時候,會依次打印出1、2、3、4。這裏特別需要注意:我們給outA綁定了多個onclick事件處理函數,也是直接點擊outA觸發的事件,所以不涉及事件冒泡和事件捕獲的問題,即addEventListener的第三個參數在這種場景下,沒有什麼用處。如果是通過事件冒泡或者是事件捕獲觸發outA的click事件,那麼函數的執行順序會有變化


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


事件冒泡和事件捕獲很好理解,只不過是對同一件事情的不同看法,只不過這2種看法都很有道理。
我們知道HTML中的元素是可以嵌套的,形成類似於樹的層次關係。比如下面的代碼:
[html] view plain
  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代表着採用事件捕獲。

[html] view plain
  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.         outA.addEventListener('click',function(){alert(1);},false);  
  10.         outB.addEventListener('click',function(){alert(2);},false);  
  11.         outC.addEventListener('click',function(){alert(3);},false);  
  12.     };  
  13.    
  14. </script>  
  15.   
  16. <body>  
  17.     <div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;">  
  18.         <div id="outB" style="height:200; background:#0000ff;top:100px;position:relative;">  
  19.             <div id="outC" style="height:100px; background:#FFB90F;top:50px;position:relative;"></div>   
  20.         </div>  
  21.     </div>  
  22. </body>  
使用的是事件冒泡,當點擊outC的時候,打印順序是3-->2-->1。如果將false改成true使用事件捕獲,打印順序是1-->2-->3。


(四) DOM事件流

DOM事件流我也不知道怎麼解釋,個人感覺就是事件冒泡和事件捕獲的結合體,直接看圖吧。


DOM事件流:將事件分爲三個階段:捕獲階段、目標階段、冒泡階段。先調用捕獲階段的處理函數,其次調用目標階段的處理函數,最後調用冒泡階段的處理函數。這個過程很類似於Struts2框中的action和Interceptor。當發出一個URL請求的時候,先調用前置攔截器,其次調用action,最後調用後置攔截器。

[html] view plain
  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("target");},true);  
  10.           
  11.         // 事件冒泡  
  12.         outA.addEventListener('click',function(){alert("bubble1");},false);  
  13.         outB.addEventListener('click',function(){alert("bubble2");},false);  
  14.           
  15.         // 事件捕獲  
  16.         outA.addEventListener('click',function(){alert("capture1");},true);  
  17.         outB.addEventListener('click',function(){alert("capture2");},true);   
  18.     };  
  19.    
  20. </script>  
  21.   
  22. <body>  
  23.     <div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;">  
  24.         <div id="outB" style="height:200; background:#0000ff;top:100px;position:relative;">  
  25.             <div id="outC" style="height:100px; background:#FFB90F;top:50px;position:relative;"></div>   
  26.         </div>  
  27.     </div>  
  28. </body>  
當點擊outC的時候,依次打印出capture1-->capture2-->target-->bubble2-->bubble1。到這裏是不是可以理解addEventListener(type,handler,useCapture)這個API中第三個參數useCapture的含義呢?useCapture=false意味着:將事件處理函數加入到冒泡階段,在冒泡階段會被調用;useCapture=true意味着:將事件處理函數加入到捕獲階段,在捕獲階段會被調用。從DOM事件流模型可以看出,捕獲階段的事件處理函數,一定比冒泡階段的事件處理函數先執行。

(五) 再談事件函數執行先後順序

在DOM事件流中提到過:

[html] view plain
  1. // 目標(自身觸發事件,是冒泡還是捕獲無所謂)  
  2. outC.addEventListener('click',function(){alert("target");},true);  
我們在outC上觸發onclick事件(這個是目標對象),如果我們在outC上同時綁定捕獲階段/冒泡階段事件處理函數會怎麼樣呢?

[html] view plain
  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事件流中的目標階段。目標階段函數的執行順序:先註冊的先執行,後註冊的後執行。這就是上面我們說的,在目標對象上綁定的函數是採用捕獲,還是採用冒泡,都沒有什麼關係,因爲冒泡和捕獲只是對父元素上的函數執行順序有影響,對自己沒有什麼影響。如果不信,可以將下面的代碼放進去驗證。

[javascript] view plain
  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()阻止事件的繼續傳播。

[html] view plain
  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阻止了事件的繼續傳播,所以不會繼續傳播到冒泡階段。


最後再看一段更有意思的代碼:

[html] view plain
  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上的事件處理函數。原因不做解釋,如果你還不明白,可以再讀一遍本文章。
發佈了43 篇原創文章 · 獲贊 37 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章