看懂此文,不再困惑於 JS 中的事件設計
抽空學習了下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:
XHTML1 | <div id="outestA"onclick="var id = this.id;alert(id);return false;"></div> |
這種做法很顯然不好,因為程式碼都是放在字串裡的,不能格式化和排版,當代碼很多的時候很難看懂。這裡有一點值得說明:onclick屬性中的this代表的是當前被點選的DOM物件,所以我們可以通過this.id獲取DOM元素的id屬性值。
方式2:
當代碼比較多的時候,我們可以在onclick等屬性中指定函式名。
跟上面的做法相比,這種做法略好一些。值得一提的是:事件處理函式中的this代表的是window物件,所以我們在onclick屬性值中,通過this將dom物件作為引數傳遞。
JavaScript12345678910 | <script>functionbuttonHandler(thisDom){alert(this.id);//undefinedalert(thisDom.id);//outestAreturnfalse;}</script><div id="outestA"onclick="return buttonHandler(this);"></div> |
方式3:在JS程式碼中通過dom元素的onclick等屬性
JavaScript123 | vardom=document.getElementById("outestA");dom.onclick=function(){alert("1="+this.id);};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物件。
JavaScript1234567 | vardom=document.getElementById("outestA");dom.attachEvent('onclick',a);functiona(){alert(this.id);//undefined } |
b) 同一個事件處理函式只能繫結一次。
JavaScript1234567 | vardom=document.getElementById("outestA");dom.attachEvent('onclick',a);dom.attachEvent('onclick',a);functiona(){alert(this.id);} |
雖然使用attachEvent綁定了2次,但是函式a只會呼叫一次。
c)不同的函式物件,可以重複繫結,不會覆蓋。 JavaScript12345 | vardom=document.getElementById("outestA");dom.attachEvent('onclick',function(){alert(1);});dom.attachEvent('onclick',function(){alert(1);});// 當outestA的click事件發生時,會彈出2個對話方塊 |
匿名函式和匿名函式是互相不相同的,即使程式碼完全一樣。所以如果我們想用detachEvent取消attachEvent繫結的事件處理函式,那麼繫結事件的時候不能使用匿名函式,必須要將事件處事函式單獨寫成一個函式,否則無法取消。
方式5:使用W3C標準的addEventListener和removeEventListener。
這2個函式是W3C標準規定的,FF和Chrome瀏覽器都支援,IE6/IE7/IE8都不支援這2個函式。不過從IE9開始就支援了這2個標準的API。 JavaScript12345 | // type:事件型別,不含"on",比如"click"、"mouseover"、"keydown";// 而attachEvent的事件名稱,含含"on",比如"onclick"、"onmouseover"、"onkeydown";// listener:事件處理函式// useCapture是事件冒泡,還是事件捕獲,預設false,代表事件冒泡型別addEventListener(type,listener,useCapture); |
a) 事件處理函式中this代表的是dom物件,不是window,這個特性與attachEvent不同。
JavaScript1234567 | vardom=document.getElementById("outestA");dom.addEventListener('click',a,false);functiona(){alert(this.id);//outestA } |
b) 同一個事件處理函式可以繫結2次,一次用於事件捕獲,一次用於事件冒泡。
JavaScript12345678910 | vardom=document.getElementById("outestA");dom.addEventListener('click',a,false);dom.addEventListener('click',a,true);functiona(){alert(this.id);//outestA }// 當點選outestA的時候,函式a會呼叫2次 |
如果繫結的是同一個事件處理函式,並且都是事件冒泡型別或者事件捕獲型別,那麼只能繫結一次。
JavaScript12345678910 | vardom=document.getElementById("outestA");dom.addEventListener('click',a,false);dom.addEventListener('click',a,false);functiona(){alert(this.id);//outestA }// 當點選outestA的時候,函式a只會呼叫1次 |
(二)事件處理函式的執行順序
方式1、方式2和方式3都不能實現事件的重複繫結,所以自然也就不存在執行順序的問題。方式4和方式5可以重複繫結特性,所以需要了解下執行順序的問題。如果你寫出依賴於執行順序的程式碼,可以斷定你的設計存在問題。所以下面的順序問題,僅作為興趣探討,沒有什麼實際意義。直接上結論:addEventListener和attachEvent表現一致,如果給同一個事件繫結多個處理函式,先繫結的先執行。下面的程式碼我在IE11、FF17和Chrome39都測試過。
XHTML1234567891011121314 | <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事件,那麼函式的執行順序會有變化。
(三) 事件冒泡和事件捕獲
事件冒泡和事件捕獲很好理解,只不過是對同一件事情的不同看法,只不過這2種看法都很有道理。 我們知道HTML中的元素是可以巢狀的,形成類似於樹的層次關係。比如下面的程式碼: XHTML12345 | <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> |
如果點選了最內側的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代表著採用事件捕獲。
XHTML12345678910111213141516171819202122 | <script>window.onload=function(){varoutA=document.getElementById("outA");varoutB=document.getElementById("outB");varoutC=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;"> |