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上的事件處理函數。緣由不作解釋,若是你還不明白,能夠再讀一遍本文章。

相關文章
相關標籤/搜索