抽空學習了下javascript和jquery的事件設計,收穫頗大,總結此貼,和大家分享。
(一)事件繫結的幾種方式
方式1:
HTML的DOM元素支援onclick、onblur等以on開頭屬性,我們可以直接在這些屬性值中編寫javascript程式碼。當點選div的時候,下面的程式碼會彈出div的ID:
這種做法很顯然不好,因為程式碼都是放在字串裡的,不能格式化和排版,當程式碼很多的時候很難看懂。這裡有一點值得說明:onclick屬性中的this代表的是當前被點選的DOM物件,所以我們可以通過this.id獲取DOM元素的id屬性值。
方式2:
當程式碼比較多的時候,我們可以在onclick等屬性中指定函式名。
跟上面的做法相比,這種做法略好一些。值得一提的是:事件處理函式中的this代表的是window物件,所以我們在onclick屬性值中,通過this將dom物件作為引數傳遞。
1 2 3 4 5 6 7 8 9 10 |
<script> function buttonHandler(thisDom) { alert(this.id);//undefined alert(thisDom.id);//outestA return false; } </script> <div id="outestA" onclick="return buttonHandler(this);"></div> |
方式3:在JS程式碼中通過dom元素的onclick等屬性
這種做法this代表當前的DOM物件。還有一點:這種做法只能繫結一個事件處理函式,後面的會覆蓋前面的。
方式4:IE下使用attachEvent/detachEvent函式進行事件繫結和取消。
attachEvent/detachEvent相容性不好,IE6~IE11都支援該函式,但是FF和Chrome瀏覽器都不支援該方法。而且attachEvent/detachEvent不是W3C標準的做法,所以不推薦使用。在IE瀏覽器下,attachEvent有以下特點。
a) 事件處理函式中this代表的是window物件,不是dom物件。
b) 同一個事件處理函式只能繫結一次。
雖然使用attachEvent繫結了2次,但是函式a只會呼叫一次。
匿名函式和匿名函式是互相不相同的,即使程式碼完全一樣。所以如果我們想用detachEvent取消attachEvent繫結的事件處理函式,那麼繫結事件的時候不能使用匿名函式,必須要將事件處事函式單獨寫成一個函式,否則無法取消。
方式5:使用W3C標準的addEventListener和removeEventListener。
a) 事件處理函式中this代表的是dom物件,不是window,這個特性與attachEvent不同。
b) 同一個事件處理函式可以繫結2次,一次用於事件捕獲,一次用於事件冒泡。
如果繫結的是同一個事件處理函式,並且都是事件冒泡型別或者事件捕獲型別,那麼只能繫結一次。
1 2 3 4 5 6 7 8 9 10 |
var dom = document.getElementById("outestA"); dom.addEventListener('click', a, false); dom.addEventListener('click', a, false); function a() { alert(this.id);//outestA } // 當點選outestA的時候,函式a只會呼叫1次 |
(二)事件處理函式的執行順序
方式1、方式2和方式3都不能實現事件的重複繫結,所以自然也就不存在執行順序的問題。方式4和方式5可以重複繫結特性,所以需要了解下執行順序的問題。如果你寫出依賴於執行順序的程式碼,可以斷定你的設計存在問題。所以下面的順序問題,僅作為興趣探討,沒有什麼實際意義。直接上結論:addEventListener和attachEvent表現一致,如果給同一個事件繫結多個處理函式,先繫結的先執行。下面的程式碼我在IE11、FF17和Chrome39都測試過。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<script> window.onload = function(){ <span style="white-space:pre"> </span>var outA = document.getElementById("outA"); outA.addEventListener('click',function(){alert(1);},false); outA.addEventListener('click',function(){alert(2);},true); outA.addEventListener('click',function(){alert(3);},true); outA.addEventListener('click',function(){alert(4);},true); }; </script> <body> <div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;"> </div> </body> |
當點選outA的時候,會依次列印出1、2、3、4。這裡特別需要注意:我們給outA繫結了多個onclick事件處理函式,也是直接點選outA觸發的事件,所以不涉及事件冒泡和事件捕獲的問題,即addEventListener的第三個引數在這種場景下,沒有什麼用處。如果是通過事件冒泡或者是事件捕獲觸發outA的click事件,那麼函式的執行順序會有變化。
(三) 事件冒泡和事件捕獲
如果點選了最內側的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代表著採用事件捕獲。
使用的是事件冒泡,當點選outC的時候,列印順序是3–>2–>1。如果將false改成true使用事件捕獲,列印順序是1–>2–>3。
(四) DOM事件流
DOM事件流我也不知道怎麼解釋,個人感覺就是事件冒泡和事件捕獲的結合體,直接看圖吧。
DOM事件流:將事件分為三個階段:捕獲階段、目標階段、冒泡階段。先呼叫捕獲階段的處理函式,其次呼叫目標階段的處理函式,最後呼叫冒泡階段的處理函式。這個過程很類似於Struts2框中的action和Interceptor。當發出一個URL請求的時候,先呼叫前置攔截器,其次呼叫action,最後呼叫後置攔截器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<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上觸發onclick事件(這個是目標物件),如果我們在outC上同時繫結捕獲階段/冒泡階段事件處理函式會怎麼樣呢?
點選outC的時候,列印順序是:capture1–>capture2–>target2–>target1–>bubble2–>bubble1。由於outC是我們觸發事件的目標物件,在outC上註冊的事件處理函式,屬於DOM事件流中的目標階段。目標階段函式的執行順序:先註冊的先執行,後註冊的後執行。這就是上面我們說的,在目標物件上繫結的函式是採用捕獲,還是採用冒泡,都沒有什麼關係,因為冒泡和捕獲只是對父元素上的函式執行順序有影響,對自己沒有什麼影響。如果不信,可以將下面的程式碼放進去驗證。
至此我們可以給出事件函式執行順序的結論了:捕獲階段的處理函式最先執行,其次是目標階段的處理函式,最後是冒泡階段的處理函式。目標階段的處理函式,先註冊的先執行,後註冊的後執行。
(六) 阻止事件冒泡和捕獲
預設情況下,多個事件處理函式會按照DOM事件流模型中的順序執行。如果子元素上發生某個事件,不需要執行父元素上註冊的事件處理函式,那麼我們可以停止捕獲和冒泡,避免沒有意義的函式呼叫。前面提到的5種事件繫結方式,都可以實現阻止事件的傳播。由於第5種方式,是最推薦的做法。所以我們基於第5種方式,看看如何阻止事件的傳播行為。IE8以及以前可以通過 window.event.cancelBubble=true阻止事件的繼續傳播;IE9+/FF/Chrome通過event.stopPropagation()阻止事件的繼續傳播。
當點選outC的時候,之後列印出capture–>target,不會列印出bubble。因為當事件傳播到outC上的處理函式時,通過stopPropagation阻止了事件的繼續傳播,所以不會繼續傳播到冒泡階段。
最後再看一段更有意思的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<script> window.onload = function(){ var outA = document.getElementById("outA"); var outB = document.getElementById("outB"); var outC = document.getElementById("outC"); // 目標 outC.addEventListener('click',function(event){alert("target");},false); // 事件冒泡 outA.addEventListener('click',function(){alert("bubble");},false); // 事件捕獲 outA.addEventListener('click',function(){alert("capture");event.stopPropagation();},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> |
執行結果是隻列印capture,不會列印target和bubble。神奇吧,我們點選了outC,但是卻沒有觸發outC上的事件處理函式,而是觸發了outA上的事件處理函式。原因不做解釋,如果你還不明白,可以再讀一遍本文章。