javascript事件機制詳解

什麼是事件?

  直觀的說就是網頁上發生的事情,按照個人概括來講就是:動做、變化、加載完成。具體來講就是鼠標點擊某些內容,通過某些元素,鍵盤按下按鍵,web頁面加載完成,以及瀏覽器窗口大小變化,滾動條滾動......javascript

  事件是構成動態網頁交互必不可少的條件,經過使用 JavaScript ,你能夠監聽特定事件的發生,並規定讓某些事件發生以對這些事件作出響應。css

javascript的三種事件模型(原始,IE,DOM2)

1.原始事件模型  

  其事件類型分爲 「輸入事件(如onclick)」 和「語義事件(如onsubmit)」。html

  經過javascript指定原始事件處理程序的方式,就是將一個函數值賦值給一個事件處理程序屬性,每一個元素(包括window 和 document)都有本身的事件處理程序屬性,這些程序一般所有小寫(如:onclick)。將這些屬性的值設置成一個函數,便可指定事件處理程序。以下所示:前端

1 var test1 = document.getElementById("test1");
2 test1.onclick = function(){
3     alert("綁定點擊事件");
4 }

  或者直接經過JS代碼做爲HTML性質值完成事件綁定:java

<a id="test1" href="javascript:void(0)" onclick="alert('點擊事件綁定')">test1</a>

  經過給當前元素的屬性指定事件處理程序,是在當前元素的做用域中運行,所以程序中的this引用的是當前元素,再看個例子:node

1  var test1 = document.getElementById("test1");
2  test1.onclick = function(){
3       alert(this.id); //"test1"
4  }

  由於事件的處理程序是做爲JS的屬性,所以可使用JS屬性顯式調用:web

document.getElementById("test1").onclick();//窗口彈出對話框:"點擊事件綁定"

  由於事件的處理程序是做爲JS的屬性,所以也能夠經過將屬性值設置爲null 的方式來達到刪除事件程序的目的chrome

document.getElementById("test1").onclick = null ;  //刪除點擊事件

 *優勢:全部瀏覽器都兼容瀏覽器

 *缺點:1)邏輯與顯示沒有分離;dom

    2)相同事件的監聽函數只能綁定一個,後綁定的會覆蓋掉前面的,如:a.onclick = func1; a.onclick = func2;將只會執行func2中的內容。

    3)沒法經過事件的冒泡、委託等機制 完成更多事情。

    在當前web程序模塊化開發以及更加複雜的邏輯情況下,這種方式顯然已經落伍了,因此在真正項目中不推薦使用,平時寫點博客小例子啥的卻是能夠,速度比較快。

IE事件模型

  在IE事件模型中,事件的表示做爲全局函數window的一個屬性event。在ie中(ie8及更早),默認的全局屬性event值爲null,而後在進入事件處理程序中時,系統將window.event 的值設置爲當前事件類型,而且在每次事件處理完成以後 又將window.event 的值設置成null。爲了求證,咱們在IE8下運行以下代碼:

1 console.log(window.event); //null 
2 window.onload = function(){
3     console.log(window.event) // Event {isTrusted: true, type: "load", target: document, currentTarget: Window, eventPhase: 2…}
4     setTimeout(function(){console.log(window.event);},100) //null
5 }

  所以咱們能夠得知

    1.在html運行時,window.event 被定義,初始值爲null。

    2.在咱們進入window.onload 函數中時,window.event 的值爲當前類型的事件。

    3.當咱們完成事件過程後,window.event 又被設置爲null。

  IE的事件模型執行過程只有兩步,對應DOM2事件的 2(事件處理),3(事件冒泡) 階段。先執行元素的監聽函數,而後事件沿着父節點一直冒泡到document。

  IE模型下的事件綁定方式與DOM不一樣,綁定監聽函數的方法是:attachEvent( "eventType","handler"),其中evetType爲事件的類型,如onclick,注意要加’on’。解除事件監聽器的方法是 detachEvent("eventType","handler" )

  與原始事件模型在當前元素上定義事件不一樣,使用attachEvent()方法的狀況下,事件的處理程序會在全局做用域中運行,所以 this 等於 window。

1 /*IE*/
2 var test1 = document.getElementById("test1");
3 test1.attachEvent("onclick",function(){
4     alert(this === window); //true
5 })
6 //attachEvent 方法能夠爲一個元素添加多個事件處理程序,後添加的先執行
7 test1.attachEvent("onclick",function(){
8     alert("helloWord"); //先helloWord後true
9 })

  *IE的事件模型已經能夠解決原始模型的三個缺點,但其本身的缺點就是兼容性,只有IE系列瀏覽器才能夠這樣寫。

DOM2事件模型(標準)

  此模型是W3C指定的標準模型,所以咱們如今使用的現代瀏覽器(IE6~IE8 除外),都遵循標準事件模型,在DOM2模型中事件有一個特殊的傳播過程,分爲三個階段: 

    (1)capturing phase:事件捕獲階段。事件被從document一直向下傳播到目標元素,在這過程當中依次檢查通過的節點是否註冊了該事件的監聽函數,如有則執行。

    (2)target phase:事件處理階段。事件到達目標元素,執行目標元素的事件處理函數.

    (3)bubbling phase:事件冒泡階段。事件從目標元素上升一直到達document,一樣依次檢查通過的節點是否註冊了該事件的監聽函數,有則執行。

  全部的事件都會經歷事件捕獲階段(capturing phase),可是隻有部分事件會經歷事件冒泡(bubbling phase),例如:submit就不會被事件冒泡。

  「DOM2事件模型」,定義了兩個方法用來指定和刪除事件處理程序:

    addEventListener("eventType","handler","true|false");和 removeEventListner("eventType","handler","true!false");

    其中eventType指事件類型,注意不要加‘on’前綴,與IE下不一樣。第二個參數是處理函數,第三個即用來指定是否在捕獲階段進行處理,通常設爲false來與IE保持一致。

1 var test1 = document.getElementById("test1");
2 test1.addEventListener("click",function(e){
3     console.log(e);  //MouseEvent
4     console.log(this); //當前監聽的元素 test1對應的標籤
5 },false)

  *當咱們瞭解了事件的三種模型以後,咱們發現他們種會存在區別。爲了更好的兼容各類瀏覽器版本,而且保證代碼能在大多數瀏覽器下一致地運行,咱們只須要關注冒泡階段,而且整理一份跨瀏覽器的事件處理程序:

 1 var EventUtil = {
 2     /**
 3      * 添加事件
 4      * @param {Object} element
 5      * @param {Object} type
 6      * @param {Object} hander
 7      */
 8     addHander: function(element, type, hander) {
 9         if (element.addEventListener) {
10             element.addEventListener(type, hander, false);
11         } else if (element.attachEvent) {
12             element.attachEvent("on" + type, hander);
13         } else {
14             element["on" + type] = hander;
15         }
16     },
17     /**
18      * 移除事件
19      * @param {Object} element
20      * @param {Object} type
21      * @param {Object} hander
22      */
23     removeHander: function(element, type, hander) {
24         if (element.removeEventListener) {
25             element.removeEventListener(type, hander, false);
26         } else if (element.detachEvent) {
27             element.detachEvent("on" + type, hander);
28         } else {
29             element["on" + type] = null;
30         }
31     },
32     /**
33      * 取得當前事件對象
34      * @param {Object} event
35      */
36     getEvent: function(event) {
37         return event ? window.event : event;
38     },
39     /**
40      * 取得觸發事件的目標元素對象
41      * @param {Object} event
42      */
43     getTarget: function(event) {
44         return event.target || event.srcElement;
45     },
46     /**
47      * 阻止默認的事件觸發
48      * @param {Object} event
49      */
50     preventDefault: function(event) {
51         if (event.preventDefault) {
52             event.preventDefault();
53         } else {
54             event.returnValue = false;
55         }
56     },
57     /**
58      * 阻止事件冒泡
59      * @param {Object} event
60      */
61     stopPropagation: function(event) {
62         if (event.stopPropagation) {
63             event.stopPropagation();
64         } else {
65             event.cancelBubble = true;
66         }
67     }
68 }

 e.target與e.currentTarget

  前文屢次使用到事件(event)的target 屬性,這裏不得不提到與之相相似的 currentTarget屬性,那麼他們分別表明什麼,而且有什麼做用和區別?下面咱們先看《javascript高級程序設計》中對着兩個屬性的說明:

 
屬性/方法 類型 讀/寫 說明
target Element 只讀 事件的目標
currentTarget Element 只讀 其事件處理程序當前正在處理事件的那個元素

  在事件處理程序內部,對象的this始終等於currentTarget的值,而target則只包含事件的實際目標。若是將事件處理程序指定給了目標元素,則 this、currentTarget、target 包含相同的值。以下例子:

1 <div id="btnPanel" style="background: #ccc;padding: 40px;">
2     <input type="button" name="btn" id="btn" value="肯定" />
3 </div>
1 var btn = document.getElementById("btn");
2 btn.onclick = function(event) {
3     alert(event.currentTarget === this); //true
4     alert(event.target === this); //true
5 }

  因爲 click 事件的目標是按鈕,而且咱們將點擊事件的處理也是指定給了按鈕。所以 currentTaget 、target 、 this 值相等。

  思考:若是咱們將 click 事件綁定到 按鈕父容器上,那麼咱們在點擊按鈕,因爲事件冒泡 觸發父容器 click事件時。 這三者有什麼變化?修改上面js部分代碼以下:

1 var btnPanel = document.getElementById("btnPanel");
2 btnPanel.onclick = function(event) {
3     alert(event.currentTarget === btnPanel); //true
4     alert(this === btnPanel);  //true
5     alert(event.target === document.getElementById("btn")); //true
6 }

  當咱們點擊這個例子中的按鈕時,this和currentTarget 都等於 這個div,由於事件處理程序是註冊到這個div上的 。 最終 target 等於按鈕元素,是由於 咱們在點擊按鈕時,因爲按鈕上沒有註冊 click 事件處理程序, 結構 click 事件就冒到了 父容器div 並觸發了click。這個按鈕 是 click 事件的真正目標。

  *總結:經過如上兩個例子,咱們能夠這樣理解:

   1.target 永遠 等於 事件的真正目標 (如click事件中,target永遠等於咱們鼠標點擊的最上層元素);

   2.currentTaget 始終等於this。等於 事件處理程序綁定的這個元素。(如: 誰.onclick = hander, 「誰」就是currentTaget,也就是最終要冒泡到「誰」上觸發事件的元素);

取消事件冒泡(stopPropagation)和取消事件的默認行爲(preventDefault)

  stopPropagation() 方法用於當即中止事件在DOM層次中的傳播,即取消進一步的事件捕獲和冒泡。 

 1 var btn = document.getElementById("btn");
 2 btn.onclick = function(event) {
 3     alert("click for btn"); 
 4     event.stopPropagation();
 5 }
 6 
 7 var btnPanel = document.getElementById("btnPanel");
 8 btnPanel.onclick = function(event) {
 9     alert("click for div"); 
10 }

  和以前例子的不一樣是,咱們在btn和div上都綁定了click事件,而且咱們在btn的事件程序中調用event.stopPropagation()。當咱們點擊按鈕時,頁面只彈出一個提示框「click for btn」,是因爲咱們在進入btn的事件時,阻止了事件在dom中的傳播,所以不會觸發 div上綁定的點擊事件。若是去掉event.stopPropagation(),則彈出兩個提示對話框 先觸發 btn事件,再經由事件冒泡 觸發div事件。

  只有cancelable 屬性爲true的事件,纔可使用preventDefault() 來取消其默認行爲。

1 <form action="http://www.baidu.com" method="post">
2     <a id="baiduA" href="http://www.baidu.com">百度</a>
3     <input id="submitBtn" type="submit" value="提交"/>
4 </form>

  咱們都知道,a標籤在點擊的時候 會觸發默認的跳轉事件,在form中點擊submit類型按鈕,會自動post而且跳轉到action指定的頁面。咱們如今要求,點擊a標籤或者submit按鈕時,只提示 不作跳轉 或者 提交。使用preventDefault();取消其默認事件的行爲:

 1 var subBtn = document.getElementById("submitBtn");
 2 subBtn.onclick = function(e){
 3     alert("subBtn");
 4     e.preventDefault();
 5 }
 6 
 7 var baiduA = document.getElementById("baiduA");
 8 baiduA.onclick = function(e){
 9     alert("baiduA");
10     e.preventDefault();
11 }

   注:在IE事件對象中,也有取消默認事件,和阻止事件冒泡的屬性。

    window.event.cancelBubble = true 對應 stoppropagation();

      window.event.returnValue  = false 對應 preventDefault();

從模擬事件淺談dispatchEvent

事件常常有用戶操做火經過其餘瀏覽器的功能來觸發,好比點擊某些按鈕,輸入某些值等等,可是,在一些特定的狀況下,咱們須要咱們的事件在任意時刻都能經過javascript來觸發。而且某些事件仍是咱們本身定義的,這個時候咱們可使用IE下fireEvent方法,高級瀏覽器(chrome,firefox等)有dispatchEvent方法。

例如在ie下看看這個例子:

 1 //document上綁定自定義事件ondataavailable
 2 document.attachEvent('ondataavailable', function(event) {
 3     alert(event.eventType);
 4 });
 5 var obj = document.getElementById("obj");
 6 //obj元素上綁定click事件
 7 obj.attachEvent('onclick', function(event) {
 8     alert(event.eventType);
 9 });
10 //調用document對象的createEventObject方法獲得一個event的對象實例。
11 var event = document.createEventObject();
12 event.eventType = 'message';
13 //觸發document上綁定的自定義事件ondataavailable
14 document.fireEvent('ondataavailable', event);
15 //觸發obj元素上綁定click事件
16 document.getElementById("test").onclick = function() {
17     obj.fireEvent('onclick', event);
18 };

再看看高級瀏覽器(chrome,firefox等)的例子:

 1 //document上綁定自定義事件ondataavailable
 2 document.addEventListener('ondataavailable', function(event) {
 3     alert(event.eventType);
 4 }, false);
 5 var obj = document.getElementById("obj");
 6 //obj元素上綁定click事件
 7 obj.addEventListener('click', function(event) {
 8     alert(event.eventType);
 9 }, false);
10 //調用document對象的 createEvent 方法獲得一個event的對象實例。
11 var event = document.createEvent('HTMLEvents');
12 // initEvent接受3個參數:
13 // 事件類型,是否冒泡,是否阻止瀏覽器的默認行爲
14 event.initEvent("ondataavailable", true, true);
15 event.eventType = 'message';
16 //觸發document上綁定的自定義事件ondataavailable
17 document.dispatchEvent(event);
18 var event1 = document.createEvent('HTMLEvents');
19 event1.initEvent("click", true, true);
20 event1.eventType = 'message';
21 //觸發obj元素上綁定click事件
22 document.getElementById("test").onclick = function() {
23     obj.dispatchEvent(event1);
24 };

在以上兩個例子中,咱們都會發現,在事件觸發的時候,咱們須要對觸發的方法傳一個事件對象進去,而且這個事件對象使咱們經過js本身建立的。

在ie 中爲:document.createEventObject();

在常規瀏覽器中:document.createEvent('HTMLEvents');

createEvent()方法返回新建立的Event對象,支持一個參數,表示事件類型,具體見下表:

參數 事件接口 初始化方法
HTMLEvents HTMLEvent initEvent()
MouseEvents MouseEvent initMouseEvent()
UIEvents UIEvent initUIEvent()

 對於標準瀏覽器,其提供了可供元素觸發的方法:element.dispatchEvent(). 不過,在使用該方法以前,咱們還須要作其餘兩件事,及建立和初始化。所以,總結說來就是:

1 document.createEvent()
2 event.initEvent()
3 element.dispatchEvent()

 *總結,對於事件的觸發,dispatchEvent 或者 fireEvent 也只是瞭解了對事件觸發的調用方式,本篇不做過多的介紹。應該會在以後單獨起一篇,事件模擬,自定義事件的文章。

 事件代理

javascript事件是全部網頁互動性的根本,所以在咱們編寫前端交互的時候,事件綁定做爲再普通不過的交互操做了。在傳統的事件處理中,你根據需求給每個元素添加或者 刪除事件處理器,然而事件處理器能夠致使內存泄露或者性能降低(你用的越多風險越大)。例如如下例子

一:當咱們點擊每一個li時,控制檯打出 當前點擊元素的 innerHtml 

1 <ul id="ul">
2   <li>aaaaaaaa</li>
3   <li>bbbbbbbb</li>
4   <li>cccccccc</li>
5 </ul>

 傳統的作法:爲每一個li綁定點擊事件:

1 window.onload = function(){
2     var ul = document.getElementById("ul");
3     var lis = ul.getElementsByTagName("li");
4     for(var i = 0 ; i < lis.length; i ++){
5         lis[i].onclick = function(){
6             console.log(this.innerHTML);
7         }
8     }
9 }

這樣咱們就能夠作到li上面添加鼠標事件,可是若是說咱們可能有不少個li用for循環的話就比較影響性能。

實際開發狀況中,咱們的li大多數是根據數據集合,動態組裝生成的。此時就須要咱們每生成一個li 就給這個li 綁定一個點擊事件。效率低,容易出錯

思考:1.有沒有一種可行的方法讓我每次只需完成一次事件綁定的操做?

   2.或者我在li的父容器上,加入某種事件,讓我在每次點擊li時,由父容器的事件判斷我當前點擊的哪一個li元素,並執行特定的操做?

答:事件代理。

下面咱們能夠用事件代理的方式來實現這樣的效果。html不變

 1 window.onload = function(){
 2     var ul = document.getElementById("ul");
 3     var lis = ul.getElementsByTagName("li");
 4     
 5     ul.onclick = function(e){
 6         /*
 7         這裏要用到事件源:event 對象,事件源,無論在哪一個事件中,只要你操做的那個元素就是事件源。
 8         ie:window.event.srcElement
 9         標準下:event.target
10         nodeName:找到元素的標籤名
11         */
12         var e  = e || window.event;
13         var target = e.target || e.srcElement;
14         if(target.nodeName.toUpperCase() == "LI"){
15             alert(target.innerHTML);
16         }
17     }
18 }

這樣,咱們就使用事件代理完成在父容器綁定點擊事件,當咱們點擊子元素li時,根據事件源獲得當前點擊的target元素。這樣就算li是動態生成的,在點擊的時候也會去獲取到新的節點li。並執行對應操做。

原理:事件冒泡以及目標元素。當一個元素的事件被觸發時,一樣的事件將會在這個元素的全部祖先元素中被觸發,這一過程稱之爲事件冒泡。這個事件從原始元素一直冒泡到dom樹最上層。而且全部的事件觸發的目標元素都是最開始的元素(target || srcElement)所以咱們能夠根據這一原理來達到觸發原始元素事件的目的。

這樣作的好處:

  1.將全部同類元素的事件操做代理給其父元素,減小了事件綁定過程,避免了100個li,1000個li 或者更多li的循環綁定事件,有效減小內存佔用,避免元素過多致使瀏覽器內存泄露,提升效率。

  2.在DOM更新後無須從新綁定事件處理器了。

接下來咱們利用事件代理封裝一個通用的方法:

 1 <!DOCTYPE html>
 2 <html>
 3 
 4     <head lang="en">
 5         <meta charset="UTF-8">
 6         <title></title>
 7         <style type="text/css" rel="stylesheet">
 8             p,
 9             h3 {
10                 width: 200px;
11                 height: 100px;
12                 border: 1px solid pink;
13                 margin: 20px auto;
14             }
15         </style>
16     </head>
17 
18     <body>
19         <div id="pElement">
20             <p>p的點擊事件</p>
21             <h3>h3的雙擊事件</h3>
22         </div>
23     </body>
24 </html>
 1 var pElement = document.getElementById('pElement');
 2 //事件代理整理
 3 EventTarget.prototype.on = function(eventType, selector, callback) {
 4     var _event = function(e) {
 5         var target = event.target || event.srcElement;
 6         if (target === selector) {
 7             callback(e);
 8         }
 9     }
10     if (this.addEventListener) {
11         this.addEventListener(eventType, _event, false);
12     } else if (this.attachEvent) {
13         this.attachEvent("on" + eventType, _event);
14     } else {
15         this["on" + eventType] = _event;
16     }
17 }
18 
19 //可擴展的選擇器 添加id class 屬性等篩選
20 function _getTarget(targetName) {
21     return document.getElementsByTagName(targetName)[0];
22 }
23 
24 //給p元素綁定點擊事件
25 pElement.on('click', _getTarget("p"), function(e) {
26     alert(e.target.innerText)
27 });
28 
29 //給h3 綁定雙擊事件
30 pElement.on('dblclick', _getTarget("h3"), function(e) {
31     alert(e.target.innerText)
32 });
相關文章
相關標籤/搜索