最詳細的JavaScript和事件解讀 與瀏覽器進行交互的時候瀏覽器就會觸發各種事件。比如當我們打開某一個網頁的時候,瀏覽器載入完成了這個網頁,就會觸發一個 load 事件;當我們點擊頁面中的某一個“地方”,瀏覽器就會在那個“地方”觸發一個 click 事件。 這樣,我們就可以編寫 JavaScri
與瀏覽器進行交互的時候瀏覽器就會觸發各種事件。比如當我們打開某一個網頁的時候,瀏覽器載入完成了這個網頁,就會觸發一個 load 事件;當我們點擊頁面中的某一個“地方”,瀏覽器就會在那個“地方”觸發一個 click 事件。
這樣,我們就可以編寫 JavaScript,通過監聽某一個事件,來實現某些功能擴展。例如監聽 load 事件,顯示歡迎信息,那麼當瀏覽器載入完一個網頁之後,就會顯示歡迎信息。
下麵就來介紹一下事件。
基礎事件操作
監聽事件
瀏覽器會根據某些操作觸發對應事件,如果我們需要針對某種事件進行處理,則需要監聽這個事件。監聽事件的方法主要有以下幾種:
HTML 內聯屬性(避免使用)
HTML 元素裡面直接填寫事件有關屬性,屬性值為 JavaScript 代碼,即可在觸發該事件的時候,執行屬性值的內容。
例如:
<button onclick="alert('你點擊了這個按鈕');">點擊這個按鈕</button>
onclick 屬性表示觸發 click,屬性值的內容(JavaScript 代碼)會在單擊該 HTML 節點時執行。
顯而易見,使用這種方法,JavaScript 代碼與 HTML 代碼耦合在了一起,不便於維護和開發。所以除非在必須使用的情況(例如統計鏈接點擊數據)下,儘量避免使用這種方法。
DOM 屬性綁定
也可以直接設置 DOM 屬性來指定某個事件對應的處理函數,這個方法比較簡單:
element.onclick = function(event){
alert('你點擊了這個按鈕');
};
上面代碼就是監聽 element 節點的 click 事件。它比較簡單易懂,而且有較好的相容性。但是也有缺陷,因為直接賦值給對應屬性,如果你在後面代碼中再次為 element 綁定一個回調函數,會覆蓋掉之前回調函數的內容。
雖然也可以用一些方法實現多個綁定,但還是推薦下麵的標準事件監聽函數。
使用事件監聽函數
標準的事件監聽函數如下:
element.addEventListener(<event-name>, <callback>, <use-capture>);
表示在 element 這個對象上面添加一個事件監聽器,當監聽到有 <event-name> 事件發生的時候,調用 <callback> 這個回調函數。至於 <use-capture> 這個參數,表示該事件監聽是在“捕獲”階段中監聽(設置為 true)還是在“冒泡”階段中監聽(設置為 false)。關於捕獲和冒泡,我們會在下麵講解。
用標準事件監聽函數改寫上面的例子:
var btn = document.getElementsByTagName('button');
btn[0].addEventListener('click', function() {
alert('你點擊了這個按鈕');
}, false);
這裡最好是為 HTML 結構定義個 id 或者 class 屬性,方便選擇,在這裡只作為演示使用。
Demo:
移除事件監聽
當我們為某個元素綁定了一個事件,每次觸發這個事件的時候,都會執行事件綁定的回調函數。如果我們想解除綁定,需要使用 removeEventListener 方法:
element.removeEventListener(<event-name>, <callback>, <use-capture>);
需要註意的是,綁定事件時的回調函數不能是匿名函數,必須是一個聲明的函數,因為解除事件綁定時需要傳遞這個回調函數的引用,才可以斷開綁定。例如:
var fun = function() {
// function logic
};
element.addEventListener('click', fun, false);
element.removeEventListener('click', fun, false);
Demo:
事件觸發過程
在上面大體瞭解了事件是什麼、如何監聽並執行某些操作,但我們對事件觸發整個過程還不夠瞭解。
下圖就是事件的觸發過程,借用了 W3C 的圖片
捕獲階段(Capture Phase)
當我們在 DOM 樹的某個節點發生了一些操作(例如單擊、滑鼠移動上去),就會有一個事件發射過去。這個事件從 Window 發出,不斷經過下級節點直到目標節點。在到達目標節點之前的過程,就是捕獲階段(Capture Phase)。
所有經過的節點,都會觸發這個事件。捕獲階段的任務就是建立這個事件傳遞路線,以便後面冒泡階段順著這條路線返回 Window。
監聽某個在捕獲階段觸發的事件,需要在事件監聽函數傳遞第三個參數 true。
element.addEventListener(<event-name>, <callback>, true);
但一般使用時我們往往傳遞 false,會在後面說明原因。
目標階段(Target Phase)
當事件跑啊跑,跑到了事件觸發目標節點那裡,最終在目標節點上觸發這個事件,就是目標階段。
需要註意的時,事件觸發的目標總是最底層的節點。比如你點擊一段文字,你以為你的事件目標節點在 div 上,但實際上觸發在 <p>、<span> 等子節點上。例如:
在 Demo 中,我監聽單擊事件,將目標節點的 tag name 彈出。當你點擊加粗字體時,事件的目標節點就為最底層的 <strong> 節點。
冒泡階段(Bubbling Phase)
當事件達到目標節點之後,就會沿著原路返回,由於這個過程類似水泡從底部浮到頂部,所以稱作冒泡階段。
在實際使用中,你並不需要把事件監聽函數準確綁定到最底層的節點也可以正常工作。比如在上例,你想為這個 <div> 綁定單擊時的回調函數,你無須為這個 <div> 下麵的所有子節點全部綁定單擊事件,只需要為 <div> 這一個節點綁定即可。因為發生它子節點的單擊事件,都會冒泡上去,發生在 <div> 上面。
針對這三個階段,wilsonpage 做了一個非常棒的 Demo,可以看下:
為什麼不用第三個參數 true
介紹完上面三個事件觸發階段,我們來看下這個問題。
所有介紹事件的文章都會說,在使用 addEventListener 函數來監聽事件時,第三個參數設置為 false,這樣監聽事件時只會監聽冒泡階段發生的事件。
這是因為 IE 瀏覽器不支持在捕獲階段監聽事件,為了統一而設置的,畢竟 IE 瀏覽器的份額是不可忽略的。
IE 瀏覽器在事件這方面與標準還有一些其他的差異,我們會在後面集中介紹。
使用事件代理(Event Delegate)提升性能
因為事件有冒泡機制,所有子節點的事件都會順著父級節點跑回去,所以我們可以通過監聽父級節點來實現監聽子節點的功能,這就是事件代理。
使用事件代理主要有兩個優勢:
- 減少事件綁定,提升性能。之前你需要綁定一堆子節點,而現在你只需要綁定一個父節點即可。減少了綁定事件監聽函數的數量。
- 動態變化的 DOM 結構,仍然可以監聽。當一個 DOM 動態創建之後,不會帶有任何事件監聽,除非你重新執行事件監聽函數,而使用事件監聽無須擔憂這個問題。
看一個例子:
上面例子中,為了簡便,我使用 jQuery 來實現普通事件綁定和事件代理。我的目標是監聽所有 a 鏈接的單擊事件,.ul1 是常規的事件綁定方法,jQuery 會迴圈每一個 .ul > a 結構並綁定事件監聽函數。.ul2 則是事件監聽的方法,jQuery 只為 .ul2 結構綁定事件監聽函數,因為 .ul2 下麵可能會有很多無關節點也會觸發 click 事件,所以我在 on 函數里傳遞了第二個參數,表示只監聽 a 子節點的事件。
它們都可以正常工作,但是當我動態創建新 DOM 結構的時候,第一個 ul 問題就出現了,新創建結構雖然還是 .ul1 > a,但是沒有綁定事件,所以無法執行回調函數。而第二個 ul 工作的很好,因為點擊新創建的 DOM ,它的事件會冒泡到父級節點進行處理。
如果使用原生的方式實現事件代理,需要註意過濾非目標節點,可以通過 id、class 或者 tagname 等等,例如:
element.addEventListener('click', function(event) {
// 判斷是否是 a 節點
if ( event.target.tagName == 'A' ) {
// a 的一些交互操作
}
}, false);
停止事件冒泡(stopPropagation)
所有的事情都會有對立面,事件的冒泡階段雖然看起來很好,也會有不適合的場所。比較複雜的應用,由於事件監聽比較複雜,可能會希望只監聽發生在具體節點的事件。這個時候就需要停止事件冒泡。
停止事件冒泡需要使用事件對象的 stopPropagation 方法,具體代碼如下:
element.addEventListener('click', function(event) {
event.stopPropagation();
}, false);
在事件監聽的回調函數里,會傳遞一個參數,這就是 Event 對象,在這個對象上調用 stopPropagation 方法即可停止事件冒泡。舉個停止事件冒泡的應用實例:
在上面例子中,有一個彈出層,我們可以在彈出層上做任何操作,例如 click 等。當我們想關掉這個彈出層,在彈出層外面的任意結構中點擊即可關掉。它首先對 document 節點進行 click 事件監聽,所有的 click 事件,都會讓彈出層隱藏掉。同樣的,我們在彈出層上面的單擊操作也會導致彈出層隱藏。之後我們對彈出層使用停止事件冒泡,掐斷了單擊事件返回 document 的冒泡路線,這樣在彈出層的操作就不會被 document 的事件處理函數監聽到。
更多關於 Event 對象的事情,我們會在下麵介紹。
事件的 Event 對象
當一個事件被觸發的時候,會創建一個事件對象(Event Object),這個對象裡面包含了一些有用的屬性或者方法。事件對象會作為第一個參數,傳遞給我們的毀掉函數。我們可以使用下麵代碼,在瀏覽器中列印出這個事件對象:
<button>列印 Event Object</button>
<script>
var btn = document.getElementsByTagName('button');
btn[0].addEventListener('click', function(event) {
console.log(event);
}, false);
</script>
就可以看到一堆屬性列表:
事件對象包括很多有用的信息,比如事件觸發時,滑鼠在屏幕上的坐標、被觸發的 DOM 詳細信息、以及上圖最下麵繼承過來的停止冒泡方法(stopPropagation)。下麵介紹一下比較常用的幾個屬性和方法:
type(string)
事件的名稱,比如 “click”。
target(node)
事件要觸發的目標節點。
bubbles (boolean)
表明該事件是否是在冒泡階段觸發的。
preventDefault (function)
這個方法可以禁止一切預設的行為,例如點擊 a 標簽時,會打開一個新頁面,如果為 a 標簽監聽事件 click 同時調用該方法,則不會打開新頁面。
stopPropagation (function)
停止冒泡,上面有提到,不再贅述。
stopImmediatePropagation (function)
與 stopPropagation 類似,就是阻止觸發其他監聽函數。但是與 stopPropagation 不同的是,它更加 “強力”,阻止除了目標之外的事件觸發,甚至阻止針對同一個目標節點的相同事件,Demo:http://jsfiddle.net/yujiangshui/ju2ujmzp/2/。
cancelable (boolean)
這個屬性表明該事件是否可以通過調用 event.preventDefault 方法來禁用預設行為。
eventPhase (number)
這個屬性的數字表示當前事件觸發在什麼階段。none:0;捕獲:1;目標:2;冒泡:3。
pageX 和 pageY (number)
這兩個屬性表示觸發事件時,滑鼠相對於頁面的坐標。Demo:http://api.jquery.com/event.pagex/。
isTrusted (boolean)
表明該事件是瀏覽器觸發(用戶真實操作觸發),還是 JavaScript 代碼觸發的。
jQuery 中的事件
如果你在寫文章或者 Demo,為了簡單,你當然可以用上面的事件監聽函數,以及那些事件對象提供的方法等。但在實際中,有一些方法和屬性是有相容性問題的,所以我們會使用 jQuery 來消除相容性問題。
下麵簡單的來說一下 jQuery 中事件的基礎操作。
綁定事件和事件代理
在 jQuery 中,提供了諸如 click() 這樣的語法糖來綁定對應事件,但是這裡推薦統一使用 on() 來綁定事件。語法:
.on( events [, selector ] [, data ], handler )
events 即為事件的名稱,你可以傳遞第二個參數來實現事件代理,具體文檔.on() 這裡不再贅述。
處理過相容性的事件對象(Event Object)
事件對象有些方法等也有相容性差異,jQuery 將其封裝處理,並提供跟標準一直的命名。
如果你想在 jQuery 事件回調函數中訪問原來的事件對象,需要使用 event.originalEvent,它指向原生的事件對象。
觸發事件 trigger 方法
點擊某個綁定了 click 事件的節點,自然會觸發該節點的 click 事件,從而執行對應回調函數。
trigger 方法可以模擬觸發事件,我們單擊另一個節點 elementB,可以使用:
$(elementB).on('click', function(){
$(elementA).trigger( "click" );
});
來觸發 elementA 節點的單擊監聽回調函數。詳情請看文檔 .trigger()。
事件進階話題
IE 瀏覽器的差異和相容性問題
IE 瀏覽器就是特立獨行,它對於事件的操作與標準有一些差異。不過 IE 瀏覽器現在也開始慢慢努力改造,讓瀏覽器變得更加標準。
IE 下綁定事件
在 IE 下麵綁定一個事件監聽,在 IE9- 無法使用標準的 addEventListener 函數,而是使用自家的 attachEvent,具體用法:
element.attachEvent(<event-name>, <callback>);
其中 <event-name> 參數需要註意,它需要為事件名稱添加 on 首碼,比如有個事件叫 click,標準事件監聽函數監聽 click,IE 這裡需要監聽 onclick。
另一個,它沒有第三個參數,也就是說它只支持監聽在冒泡階段觸發的事件,所以為了統一,在使用標準事件監聽函數的時候,第三參數傳遞 false。
當然,這個方法在 IE9 已經被拋棄,在 IE11 已經被移除了,IE 也在慢慢變好。
IE 中 Event 對象需要註意的地方
IE 中往回調函數中傳遞的事件對象與標準也有一些差異,你需要使用 window.event 來獲取事件對象。所以你通常會寫出下麵代碼來獲取事件對象:
event = event || window.event
此外還有一些事件屬性有差別,比如比較常用的 event.target 屬性,IE 中沒有,而是使用 event.srcElement 來代替。如果你的回調函數需要處理觸發事件的節點,那麼需要寫:
node = event.srcElement || event.target;
常見的就是這點,更細節的不再多說。在概念學習中,我們沒必要為不標準的東西支付學習成本;在實際應用中,類庫已經幫我們封裝好這些相容性問題。可喜的是 IE 瀏覽器現在也開始不斷向標準進步。
事件回調函數的作用域問題
與事件綁定在一起的回調函數作用域會有問題,我們來看個例子:
Events in JavaScript: Removing event listeners
回調函數調用的 user.greeting 函數作用域應該是在 user 下的,本期望輸出 My name is Bob 結果卻輸出了 My name is undefined。這是因為事件綁定函數時,該函數會以當前元素為作用域執行。為了證明這一點,我們可以為當前 element 添加屬性:
element.firstname = 'jiangshui';
再次點擊,可以正確彈出 My name is jiangshui。那麼我們來解決一下這個問題。
使用匿名函數
我們為回調函數包裹一層匿名函數。
Events in JavaScript: Removing event listeners
包裹之後,雖然匿名函數的作用域被指向事件觸發元素,但執行的內容就像直接調用一樣,不會影響其作用域。
使用 bind 方法
使用匿名函數是有缺陷的,每次調用都包裹進匿名函數裡面,增加了冗餘代碼等,此外如果想使用 removeEventListener 解除綁定,還需要再創建一個函數引用。Function 類型提供了 bind 方法,可以為函數綁定作用域,無論函數在哪裡調用,都不會改變它的作用域。通過如下語句綁定作用域:
user.greeting = user.greeting.bind(user);
這樣我們就可以直接使用:
element.addEventListener('click', user.greeting);
常用事件和技巧
用戶的操作有很多種,所以有很多事件。為了開發方便,瀏覽器又提供了一些事件,所以有很多很多的事件。這裡只介紹幾種常用的事件和使用技巧。
load
load 事件在資源載入完成時觸發。這個資源可以是圖片、CSS 文件、JS 文件、視頻、document 和 window 等等。
比較常用的就是監聽 window 的 load 事件,當頁面內所有資源全部載入完成之後就會觸發。比如用 JS 對圖片以及其他資源處理,我們在 load 事件中觸發,可以保證 JS 不會在資源未載入完成就開始處理資源導致報錯。
同樣的,也可以監聽圖片等其他資源載入情況。
beforeunload
當瀏覽者在頁面上的輸入框輸入一些內容時,未保存、誤操作關掉網頁可能會導致輸入信息丟失。
當瀏覽者輸入信息但未保存時關掉網頁,我們就可以開始監聽這個事件,例如:
window.addEventListener("beforeunload", function( event ) {
event.returnValue = "放棄當前未保存內容而關閉頁面?";
});
這時候試圖關閉網頁的時候,會彈窗阻止操作,點擊確認之後才會關閉。當然,如果沒有必要,就不要監聽,不要以為使用它可以為你留住瀏覽者。
resize
當節點尺寸發生變化時,觸發這個事件。通常用在 window 上,這樣可以監聽瀏覽器視窗的變化。通常用在複雜佈局和響應式上。
常見的視差滾動效果網站以及同類比較複雜的佈局網站,往往使用 JavaScript 來計算尺寸、位置。如果用戶調整瀏覽器大小,尺寸、位置不隨著改變則會出現錯位情況。在 window 上監聽該事件,觸發時調用計算尺寸、位置的函數,可以根據瀏覽器的大小來重新計算。
但需要註意一點,當瀏覽器發生任意變化都會觸發 resize 事件,哪怕是縮小 1px 的瀏覽器寬度,這樣調整瀏覽器時會觸發大量的 resize 事件,你的回調函數就會被大量的執行,導致變卡、崩潰等。
你可以使用函數 throttle 或者 debounce 技巧來進行優化,throttle 方法大體思路就是在某一段時間內無論多次調用,只執行一次函數,到達時間就執行;debounce 方法大體思路就是在某一段時間內等待是否還會重覆調用,如果不會再調用,就執行函數,如果還有重覆調用,則不執行繼續等待。關於它們更詳細的信息,我後面會介紹一下發表在我的博客上,這裡不再贅述。
error
當我們載入資源失敗或者載入成功但是只載入一部分而無法使用時,就會觸發 error 事件,我們可以通過監聽該事件來提示一個友好的報錯或者進行其他處理。比如 JS 資源載入失敗,則提示嘗試刷新;圖片資源載入失敗,在圖片下麵提示圖片載入失敗等。該事件不會冒泡。因為子節點載入失敗,並不意味著父節點載入失敗,所以你的處理函數必須精確綁定到目標節點。
需要註意的是,對於該事件,你可以使用 addEventListener 等進行監聽,但是有時候會出現失效情況(看這個例子),這是因為 error 事件都觸發過了,你的 JS 監聽處理代碼還沒有載入進來執行。為了避免這種情況,用內聯法更好一些:
<img src="not-found.jpg" onerror="doSomething" />
如果還有其他常用事件,歡迎留言補充。
用 JavaScript 模擬觸發內置事件
內置的事件也可以被 JavaScript 模擬觸發,比如下麵函數模擬觸發單擊事件:
function simulateClick() {
var event = new MouseEvent('click', {
'view': window,
'bubbles': true,
'cancelable': true
});
var cb = document.getElementById('checkbox');
var canceled = !cb.dispatchEvent(event);
if (canceled) {
// A handler called preventDefault.
alert("canceled");
} else {
// None of the handlers called preventDefault.
alert("not canceled");
}
}
可以看這個 Demo 來瞭解更多。
自定義事件
我們可以自定義事件來實現更靈活的開發,事件用好了可以是一件很強大的工具,基於事件的開發有很多優勢(後面介紹)。
與自定義事件的函數有 Event、CustomEvent 和 dispatchEvent。
直接自定義事件,使用 Event 構造函數:
var event = new Event('build');
// Listen for the event.
elem.addEventListener('build', function (e) { ... }, false);
// Dispatch the event.
elem.dispatchEvent(event);
CustomEvent 可以創建一個更高度自定義事件,還可以附帶一些數據,具體用法如下:
var myEvent = new CustomEvent(eventname, options);
其中 options 可以是:
{
detail: {
...
},
bubbles: true,
cancelable: false
}
其中 detail 可以存放一些初始化的信息,可以在觸發的時候調用。其他屬性就是定義該事件是否具有冒泡等等功能。
內置的事件會由瀏覽器根據某些操作進行觸發,自定義的事件就需要人工觸發。dispatchEvent 函數就是用來觸發某個事件:
element.dispatchEvent(customEvent);
上面代碼表示,在 element 上面觸發 customEvent 這個事件。結合起來用就是:
// add an appropriate event listener
obj.addEventListener("cat", function(e) { process(e.detail) });
// create and dispatch the event
var event = new CustomEvent("cat", {"detail":{"hazcheeseburger":true}});
obj.dispatchEvent(event);
使用自定義事件需要註意相容性問題,而使用 jQuery 就簡單多了:
// 綁定自定義事件
$(element).on('myCustomEvent', function(){});
// 觸發事件
$(element).trigger('myCustomEvent');
此外,你還可以在觸發自定義事件時傳遞更多參數信息:
$( "p" ).on( "myCustomEvent", function( event, myName ) {
$( this ).text( myName + ", hi there!" );
});
$( "button" ).click(function () {
$( "p" ).trigger( "myCustomEvent", [ "John" ] );
});
更詳細的用法請看 Introducing Custom Events,這裡不再贅述。
在開發中應用事件
當我們操作某一個 DOM,發出一個事件,我們可以在另一個地方寫代碼捕獲這個事件執行處理邏輯。觸發操作和捕獲處理操作是分開的。我們可以根據這個特性來對程式解耦。
用事件解耦
我們可以將一個整個的功能,分割成獨立的小功能,每個小功能綁定一個事件,由一個“控制器”負責根據條件觸發某個事件。這樣,在外面觸發這個事件,也可以調用對應功能,使其更加靈活。
在《基於 MVC 的 JavaScript Web 富應用開發》一書中,有更加具體的實例,有興趣的朋友可以買本看看。
發佈(Publish)和訂閱(Subscribe)模式
針對上面這種用法,繼續抽象一下,就是發佈和訂閱開發模式。正如其名,這種模式有兩個角色:發佈者和訂閱者,此外有一條通道,發佈者被觸發往這個通道裡面發信,訂閱者從這個通道裡面收信,如果收到特定信件則執行某個對應的邏輯。這樣,發佈者和訂閱者之間是完全解耦的,只有一條通道連接。這樣就非常容易擴展,也不會引入額外的依賴。
這樣如果需要添加新功能,只需要添加一個新的訂閱者(及其執行邏輯),監聽通道中某一類新的信件。再在應用中通過發佈者發送一類新的信件即可。
具體實現,這裡推薦 cowboy 開發的 Tiny Pub Sub,通過 jQuery 實現,非常簡潔直觀,jQuery