瀏覽器中關于事件的那點事兒
在前端中,有一個很重要的概念就是事件。我對于事件的理解就是使用者對瀏覽器進行的一個動作,或者說一個操作。
本文會介紹很多與事件有關的東西,雖然我的出發點有那么點一網打盡的意思m不過也難以蓋全,所以就把最常用,最基本也相對重要的內容拿出來記錄一下。
Javascript綁定事件的方式
傳統的事件綁定
因為各種歷史原因,事件的綁定在不同的瀏覽器總是有不同的寫法,當然現在可能大多數人都已經習慣于jQuery的事件綁定,而不清楚javascript的原生事件綁定是什么樣子。
非常傳統的事件的綁定方式,是在一個元素上直接綁定方法,element.onclick = function(e){}
<body> <input type="button" id="bt" name="bt button" value="this is a button"> <script> var bt = document.getElementById("bt"); bt.onclick = function(e){ alert("this is a alert"); alert(e.currentTarget.name); } </script> </body>
這是傳統的事件綁定,它非常簡單而且穩定,適應不同瀏覽器。e表示事件,this指向當前元素。但是這樣的綁定只會在事件冒泡中運行,捕獲不行。一個元素一次只能綁定一個事件函數。
W3C方式的事件綁定
W3C中推薦使用的事件綁定是用addEventListener()函數,element.addEventListener('click',function(e){...},false),上代碼:
<body> <input type="button" id="bt" name="bt button" value="this is a button"> <script> var bt = document.getElementById("bt"); bt.addEventListener('click',function(e){ alert("this is a alert"); alert(e.currentTarget.name); },false); </script> </body>
如此的效果和之前的傳統綁定方式是一樣的,這種綁定同時支持捕獲和冒泡,addEventListener()函數最后的參數表達了事件處理的階段——false(冒泡),true(捕獲)。這種方式最重要的好處就是對同一元素的同一個類型事件做綁定不會覆蓋,會全部生效。比如對上面代碼bt元素在進行一次click的綁定,那么兩次綁定的事件處理函數都會執行,按照代碼書寫順序。
但是IE瀏覽器不支持addEventListener()函數,只能在IE9以上(包括IE9)可以使用,IE瀏覽器相應的要使用attachEvent()函數代替。
IE下的事件綁定
IE下事件綁定的函數是attachEvent,它支持全系列的IE,但是如果你在Chrome等其他內核瀏覽器中使用這個函數去綁定事件,瀏覽器會報錯的。
<body> <input type="button" id="bt" name="bt button" value="this is a button"> <script> var name = "world"; var bt = document.getElementById("bt"); bt.attachEvent('onclick',function(){ alert("hello "+ this.name); }); </script> </body>
attachEvent()函數支持事件捕獲的冒泡階段,同時它不會覆蓋事件的綁定。但是一個缺點就是它處理函數中的this指向的是全局的window,所以上面代碼彈出的結果會是"hello world"。
冒泡和捕獲
上面的綁定事件中提到了冒泡和捕獲階段的概念,這兩個概念對于理解事件是很重要的,對于它們的理解還要涉及到DOM(文檔對象模型)和事件流的概念。事件流就是一個事件對象沿著特定數據結構傳播的這么一個過程。
所謂的事件對象就是Event,當一個元素上綁定的事件被觸發時會產生一個事件對象,從一切皆對象的觀點看這是很符合邏輯的。冒泡和捕獲講的就是事件流在DOM中兩種不同的傳播方式。對于冒泡和捕獲的理解,我們還是從一個小的示例來看。
<body> <div id="bt1" style="width:300px;height:300px;border:1px solid red" name="divbt1"> <div id="bt2" style="width:100px;height:100px;border:1px solid red" name="divbt2"></div> </div> <script> var bt1 = document.getElementById("bt1"); bt1.onclick = function(e){ alert("bt1"); } var bt2 = document.getElementById("bt2"); bt2.onclick = function(e){ alert("bt2"); } </script> </body>
這里我們使用最簡單的,最原始的事件綁定方式。2個div嵌套并且綁定有彈窗事件,那么當我們點擊里面的div的時候,兩個div的點擊事件都會被觸發這個是沒有疑問的,那么它們的處理函數誰先被執行?
這里用IE8/9/10和Chrome瀏覽器同時實驗,結果都是先彈出bt2,然后彈出bt1,也就是里面小div的事件先被處理了。我們來思考一下這是什么樣的一個順序,從DOM的結構上看,應該是這樣的body > bt1 > bt2。我們把這個結構豎過來,bt2在整個結構的最下面,body在最上面。想象一下,當點擊發生時產生一個泡泡(也就是事件對象),然后這個泡泡慢慢,向上浮,首先路過bt2,然后路過bt1,在路過它們時依次執行事件函數,這就是冒泡型事件。
與之相反的就是捕獲型事件,它的事件流傳播的順序正好與冒泡型事件完全相反。也就是bt1上的事件先觸發,然后傳遞到bt2。捕獲是由表及里,冒泡是由內之外。
那么現在回憶一下之前的W3C標準中那個addEventListener()函數,它里面最后一個參數false代表冒泡,true代表捕獲,這是什么意思呢?因為W3C作為一個標準,它選擇了一個相對折中的方案去處理事件,也就是任何在W3C事件模型中發生的事件都先進入捕獲階段,然后在進入冒泡階段,保證一個事件會經歷這兩個階段,以適應IE和其他非IE瀏覽器,這樣使用者可以根據需求選擇將事件注冊到哪一個階段。
現在再來看用addEventListener()函數進行事件綁定的結果:
<body> <div id="bt1" style="width:300px;height:300px;border:1px solid red" name="divbt1"> <div id="bt2" style="width:100px;height:100px;border:1px solid red" name="divbt2"></div> </div> <script> var bt1 = document.getElementById("bt1"); bt1.addEventListener('click',function(e){ alert("bt1"); },false); var bt2 = document.getElementById("bt2"); bt2.addEventListener('click',function(e){ alert("bt2"); },false); </script> </body>
這里2個div的事件綁定類型一共有4個可能的組合:2個false;2個true;1個false,1個true;1個true,1個false。這里分別試驗下吧,記住按照W3C標準,捕獲階段會在冒泡之前。
jQuery綁定事件的方式
上面我們記錄了關于javascript原生的事件綁定的一些寫法,這里我們在介紹一下通過jQuery進行事件綁定的方式。首先來夸一夸 jQuery的好,通過jQuery綁定讓我們省去了考慮瀏覽器兼容和事件流程序的相關細節內容。jQuery中對于事件的綁定稱為委托,這是一個很好的定義,所謂委托,顧名思義就是自己不去做,我讓別人幫我做這個事。jQuery就是這么做的,讓我們詳細了解下。
.bind()
我們直接看代碼,bind()函數使用很簡單。
<body> <div id="div1" style="width:300px;height:300px;border:1px solid red" name="divbt1"> <script> $("#div1").bind('click',function(e){ alert("div1 " + e.currentTarget.name); }); </script> </body>
代碼在IE8, IE11, Chrome運行都沒有問題,我們簡單翻譯一下就是首先找到id為div1的div對象,然后給這個對象綁定一個 click事件。現在來分析一下bind(),首先,如果用它綁定事件要有一個尋找jQuery對象的過程;其次,如果要為大量的元素綁定事件那么要尋找大量的對象不說,每一個對象還要占用內存來存儲相應的處理函數。并且bind()只能為當前已存在的DOM節點綁定事件,如果節點還沒有產生bind是沒有辦法的。
所以說bind()推薦在使用比較簡單的情況中,綁定不多的節點并且沒有新節點產生的情況,如果比較復雜就推薦使用delegate()。
.delegate()
在jQuery中還有一個live()函數也能處理類似的問題,但是不如delegate()好用,所以這里就不介紹了。delegate()是為了突破單一bind()方法的局限性,實現事件的委托。我們先看代碼來理解:
<body> <div id="div1" style="width:300px;height:300px;border:1px solid red" name="divbt1"> <div id="div2" style="width:100px;height:100px;border:1px solid red" name="divbt2"></div> </div> <script> $("body").delegate('#div1','click',function(e){ alert("div1"); }); $("body").delegate('#div2','click',function(e){ alert("div2"); }); </script> </body>
解讀一下delegate()函數,我們尋找到body標簽的對象并調用delegate(),這是把事件的執行委托給body。也就是監聽整個DOM樹,當觸發事件的DOM節點的是id為div1,觸發觸發事件的類型是click時,在事件傳播到body時,我們執行相應的處理函數。body怎么能知道這么多,它如何知道綁定在它身上的執行函數什么時候執行?jQuery這些事件委托的原理根據事件冒泡的機制,廣播的時候所有的節點都會知道,到底發生了什么。
DOM在為頁面中的每個元素分派事件時,相應的元素一般都在事件冒泡階段處理事件。在類似 body > div > a 這樣的結構中,如果單擊a元素,click事件會從a一直冒泡到div和body(即document對象)。因此,發生在a上面的單擊事件,div和body元素同樣可以處理。而利用事件傳播(這里是冒泡)這個機制,就可以實現事件委托。具體來說,事件委托就是事件目標自身不處理事件,而是把處理任務委托給其父元素或者祖先元素,甚至根元素(document)。
事件的取消
在一些情況下我們需要阻止事件流的傳播,或者解除之前綁定的事件。在實際工作中經常會遇到類似的需求,尤其是事件流的阻止。
事件流阻止
某些事件的對象是可以取消的,這意味著可以阻止默認動作的發生。事件對象是否可以取消,要通過Event.cancelable屬性表示。事件監聽器可以調用Event.preventDefault()取消事件對象的默認動作。Event.stopPropagation()方法可以阻止事件向上冒泡。
事件的阻止根據場景不同和瀏覽器不同有不同的處理,因為事件處理模型不同的關系。如果在IE下,Event.returnValue = false就可以;如果是非IE下,用Event.preventDefault()阻止。事件流阻止,這里面阻止的是它的繼續傳播以及有可能產生的默認動作。這里舉一個常見且簡單的例子,就是submit類型按鈕的點擊。
<body> <form action="asd.action"> <input type="submit" id="tijiao" value="submit"/> </form> <script> $("body").delegate('#tijiao','click',function(e){ e.preventDefault(); }); </script> </body>
這里點擊按鈕,form表單默認的提交被阻止了,也就是其默認動作終止了。這里有一個強調的就是滾動事件,滾動也是經常遇到需要處理的事件類型,但是滾動的阻止有點特例,它不支持在委托里進行阻止。
說到這里我們感覺Event.preventDefault()和Event.stopPropagation()都可以阻止事件,那么它們有什么區別?
前者是通知瀏覽器不要執行與事件相關聯的默認動作,比如submit類型的按鈕點擊會提交;后者是停止事件流的繼續冒泡,但是它對IE8及以下IE瀏覽器支持不好。如果直接使用return false則表示終止處理函數。
事件函數的解除綁定
和事件的綁定其實是相對應的,如果需要接觸事件的綁定,運行對應的函數就可以了。如果是原生JS綁定則對應運行removeEventListener()和detachEvent()。看一個代碼示例:
var EventUtil = { //注冊 addHandler: function(element, type, handler){ if (element.addEventListener){ element.addEventListener(type, handler, false); } else if (element.attachEvent){ element.attachEvent("on" + type, handler); } else { element["on" + type] = handler; } }, //移除注冊 removeHandler: function(element, type, handler){ if (element.removeEventListener){ element.removeEventListener(type, handler, false); } else if (element.detachEvent){ element.detachEvent("on" + type, handler); } else { element["on" + type] = null; } } };
如果是jQuery的綁定,也是存在對應的解綁函數用以清除注冊事件,比如unbind()和undelegate()。