Click、touch、load、drag、change、input、error、risize — 這些都是冗長的DOM(文檔對象模型)事件列表的一部分。事件能夠在文檔(Document)結構的任何部分被觸發,觸發者能夠是用戶操做,也能夠是瀏覽 器自己。事件並非只是在一處被觸發和終止;他們在整個document中流動,擁有它們本身的生命週期。而這個生命週期讓DOM事件有更多的用途和可擴 展性。javascript
做爲一個開發人員,咱們必需要理解DOM事件是如何工做的,而後才能更好的駕馭它,利用它們潛在的優點,開發出更高交互性的參與體驗(engaging experiences)。css
反觀我作前端開發的這麼長時間裏,我以爲我歷來沒有看到過一個關於DOM事件是如何工做的較爲直接準確的解釋。今天個人目標就是在這個課題上給你們一 個清晰的介紹,讓你們可以更快速的瞭解它。 我首先會介紹DOM事件的基本使用方式,而後會深刻挖掘事件內部的工做機制,解釋咱們如何使用這些機制來解決一些常見的問題。html
在過去,主流瀏覽器之間對於如何給DOM節點添加事件監聽有着很大的不一致性。jQuery這樣的前端庫爲咱們封裝和抽象了這些差別行爲,爲事件處理帶來了極大的便利。前端
現在,咱們正一步步走向一個標準化的瀏覽器時代,咱們能夠更加安全地使用官方規範的接口。爲了簡單起見,這篇文章將主要介紹在現代瀏覽器中如何管理事件。若是你在爲IE8或者更低版本寫JavaScript,我會推薦你使用polyfill或者一些框架(如jQuery)來管理事件監聽。java
在JavaScript中,咱們使用以下的方式爲元素添加事件監聽:node
1
|
element.addEventListener(<event-name>, <callback>, <use-capture>);
|
1
2
3
4
5
6
7
8
|
var element = document.getElementById( 'element' );
function callback() {
alert( 'Hello' );
}
// Add listener
element.addEventListener( 'click' , callback);
|
Demo: addEventListenerjquery
移除再也不使用的事件監聽是一個最佳實踐(尤爲對於長時間運行的Web應用)。咱們使用element.removeEventListener()方法來移除事件監聽:git
1
|
element.removeEventListener(<event-name>, <callback>, <use-capture>);
|
可是removeElementListener有一點須要注意的是:你必需要有這個被綁定的回調函數的引用。簡單地調用element.removeEventListener('click');是不能達到想要的效果的。github
本質上來說,若是咱們考慮要移除事件監聽(咱們在長時間運行(long-lived)的應用中須要用到),那麼咱們就須要保留回調函數的句柄。意思就是說,咱們不能使用匿名函數做爲回調函數。web
1
2
3
4
5
6
7
8
9
|
var element = document.getElementById( 'element' );
function callback() {
alert( 'Hello once' );
element.removeEventListener( 'click' , callback);
}
// Add listener
element.addEventListener( 'click' , callback);
|
一個很容易遇到的問題就是回調函數沒有在預想的運行上下文被調用。讓咱們看一個簡單的例子來解釋一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
var element = document.getElementById( 'element' );
var user = {
firstname: 'Wilson' ,
greeting: function (){
alert( 'My name is ' + this .firstname);
}
};
// Attach user.greeting as a callback
element.addEventListener( 'click' , user.greeting);
// alert => 'My name is undefined'
|
Demo: Incorrect callback context
咱們但願回調函數中可以正確的輸出」My name is Wilson」。事實上,結果確是」My name is undefined」。爲了使得 this.firstName 可以返回」Wilson」,user.greeting必須在user對象的上下文環境(context)中被執行(這裏的運行上下文指的是.號左邊的對 象)。
當咱們將greeting函數傳給addEventListener方法的時候,咱們傳遞的是一個函數的引用;user相應的上下文並無傳遞過去。 運行的時候,這個回調函數其實是在element的上下文中被執行了,也就是說,在運行的時候,this指向的是element,而不是user。因此 this.firstName是undefined。
有兩種方式能夠避免這種上下文錯誤的問題。第一種方法,咱們能夠在一個匿名函數內部調用user.greeting()方法,從而得到正確的函數執行上下文(user)。
1
2
3
4
|
element.addEventListener( 'click' , function () {
user.greeting();
// alert => 'My name is Wilson'
});
|
上一種方式並非很是好,由於咱們不能得到回調函數的句柄以便後面經過.removeEventListener()移除事件監聽。另外,這種方式也 比較醜陋。。我更喜歡使用.bind()方法(作爲ECMAScript 5的標準內建在全部的函數對象中)來生成一個新的函數(被綁定過的函數),這個函數會在指定的上下文中被執行。而後咱們將這個被綁定過的函數做爲參數傳 給.addEventListener()的回調函數。
1
2
3
4
5
6
|
// Overwrite the original function with
// one bound to the context of 'user'
user.greeting = user.greeting.bind(user);
// Attach the bound user.greeting as a callback
button.addEventListener( 'click' , user.greeting);
|
與此同時,咱們得到了回調函數的句柄,從而能夠隨時從元素上移除相應的事件監聽。
1
|
button.removeEventListener( 'click' , user.greeting);
|
想獲取Function.prototype.bind的更多信息,請點擊的瀏覽器支持頁面,以及polyfill的介紹。
Event對象在event第一次觸發的時候被建立出來,而且一直伴隨着事件在DOM結構中流轉的整個生命週期。event對象會被做爲第一個參數傳遞給事件監聽的回調函數。咱們能夠經過這個event對象來獲取到大量當前事件相關的信息:
此外事件對象還可能擁有不少其餘的屬性,可是他們都是針對特定的event的。好比,鼠標事件包含clientX和clientY屬性來代表鼠標在當前視窗的位置。
咱們可使用熟悉的瀏覽器的調試工具或者經過console.log在控制檯輸出來更具體地查看事件對象以及它的屬性。
當一個DOM事件被觸發的時候,它並不僅是在它的起源對象上觸發一次,而是會經歷三個不一樣的階段。簡而言之:事件一開始從文檔的根節點流向目標對象(捕獲階段),而後在目標對向上被觸發(目標階段),以後再回溯到文檔的根節點(冒泡階段)。
(圖片來源:W3C)
事件的第一個階段是捕獲階段。事件從文檔的根節點出發,隨着DOM樹的結構向事件的目標節點流去。途中通過各個層次的DOM節點,並在各節點上觸發捕 獲事件,直到到達事件的目標節點。捕獲階段的主要任務是創建傳播路徑,在冒泡階段,事件會經過這個路徑回溯到文檔跟節點。
正如文章一開始的地方提到,咱們能夠經過將addEventListener的第三個參數設置成true來爲事件的捕獲階段添加監聽回調函數。在實際 應用中,咱們並無太多使用捕獲階段監聽的用例,可是經過在捕獲階段對事件的處理,咱們能夠阻止相似clicks事件在某個特定元素上被觸發。
1
2
3
4
5
|
var form = document.querySelector( 'form' );
form.addEventListener( 'click' , function (event) {
event.stopPropagation();
}, true ); // Note: 'true'
|
若是你對這種用法不是很瞭解的話,最好仍是將useCapture設置爲false或者undefined,從而在冒泡階段對事件進行監聽。
當事件到達目標節點的,事件就進入了目標階段。事件在目標節點上被觸發,而後會逆向迴流,直到傳播至最外層的文檔節點。
對於多層嵌套的節點,鼠標和指針事件常常會被定位到最裏層的元素上。假設,你在一個<div>元素上設置了click事件的監聽函數,而 用戶點擊在了這個<div>元素內部的<p>元素上,那麼<p>元素就是這個事件的目標元素。事件冒泡讓咱們能夠在 這個<div>(或者更上層的)元素上監聽click事件,而且事件傳播過程當中觸發回調函數。
事件在目標元素上觸發後,並不在這個元素上終止。它會隨着DOM樹一層層向上冒泡,直到到達最外層的根節點。也就是說,同一個事件會依次在目標節點的父節點,父節點的父節點。。。直到最外層的節點上被觸發。
將DOM結構想象成一個洋蔥,事件目標是這個洋蔥的中心。在捕獲階段,事件從最外層鑽入洋蔥,穿過途徑的每一層。在到達中心後,事件被觸發(目標階段)。而後事件開始回溯,再次通過每一層返回(冒泡階段)。當到達洋蔥表面的時候,此次旅程就結束了。
冒泡過程很是有用。它將咱們從對特定元素的事件監聽中釋放出來,相反,咱們能夠監聽DOM樹上更上層的元素,等待事件冒泡的到達。若是沒有事件冒泡,在某些狀況下,咱們須要監聽不少不一樣的元素來確保捕獲到想要的事件。
Demo: Identifying event phases
絕大多數事件會冒泡,但並不是全部的。當你發現有些事件不冒泡的時候,它確定是有緣由的。不相信?你能夠查看一下相應的規範說明。
能夠經過調用事件對象的stopPropagation方法,在任何階段(捕獲階段或者冒泡階段)中斷事件的傳播。此後,事件不會在後面傳播過程當中的通過的節點上調用任何的監聽函數。
1
2
3
4
5
6
7
8
|
child.addEventListener( 'click' , function (event) {
event.stopPropagation();
});
parent.addEventListener( 'click' , function (event) {
// If the child element is clicked
// this callback will not fire
});
|
調用event.stopPropagation()
不會阻止當前節點上此事件其餘的監聽函數被調用。若是你但願阻止當前節點上的其餘回調函數被調用的話,你可使用更激進的event.stopImmediatePropagation()
方法。
1
2
3
4
5
6
7
8
|
child.addEventListener( 'click' , function (event) {
event.stopImmediatePropagation();
});
child.addEventListener( 'click' , function (event) {
// If the child element is clicked
// this callback will not fire
});
|
當特定事件發生的時候,瀏覽器會有一些默認的行爲做爲反應。最多見的事件不過於link被點擊。當一個click事件在一個<a>元素上 被觸發時,它會向上冒泡直到DOM結構的最外層document,瀏覽器會解釋href屬性,而且在窗口中加載新地址的內容。
在web應用中,開發人員常常但願可以自行管理導航(navigation)信息,而不是經過刷新頁面。爲了實現這個目的,咱們須要阻止瀏覽器針對點擊事件的默認行爲,而使用咱們本身的處理方式。這時,咱們就須要調用event.preventDefault().
1
2
3
4
|
anchor.addEventListener( 'click' , function (event) {
event.preventDefault();
// Do our own thing
});
|
咱們能夠阻止瀏覽器的不少其餘默認行爲。好比,咱們能夠在HTML5遊戲中阻止敲擊空格時的頁面滾動行爲,或者阻止文本選擇框的點擊行爲。
調用event.stopPropagation()只會阻止傳播鏈中後續的回調函數被觸發。它不會阻止瀏覽器的自身的行爲。
Demo:Preventing default vehaviour
瀏覽器並非惟一能觸發DOM事件的載體。咱們能夠建立自定義的事件並把它們分派給你文檔中的任意節點。這些自定義的事件和一般的DOM事件有相同的行爲。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
var myEvent = new CustomEvent( "myevent" , {
detail: {
name: "Wilson"
},
bubbles: true ,
cancelable: false
});
// Listen for 'myevent' on an element
myElement.addEventListener( 'myevent' , function (event) {
alert( 'Hello ' + event.detail.name);
});
// Trigger the 'myevent'
myElement.dispatchEvent(myEvent);
|
在元素上合成不可信任的(untrusted)DOM事件(如click)來模擬用戶操做也是可行的。這個在對DOM相關的代碼庫進行測試的時候特別有用。若是你對此感興趣的話,在Mozilla Developer Network上有一篇相關的文章。
幾個注意點:
代理事件監聽可讓你使用一個事件監聽器去監聽大量的DOM節點的事件,在這種狀況下,它是一種更加方便而且高性能的事件監聽方法。舉例來講,若是有 一個列表<ul>包含了100個子元素<li>,它們都須要對click事件作出類似的響應,那麼咱們可能須要查詢這100個子 元素,並分別爲他們添加上事件監聽器。這樣的話,咱們就會產生100個獨立的事件監聽器。若是有一個新的元素被添加進去,咱們也須要爲它添加一樣的監聽 器。這種方式不但代價比較大,維護起來也比較麻煩。
代理事件監聽可讓咱們更簡單的處理這種狀況。咱們不去監聽全部的子元素的click事件,相反,咱們監聽他們的父元素<ul>。當一 個<li>元素被點擊的時候,這個事件會向上冒泡至<ul>,觸發回調函數。咱們能夠經過檢查事件的event.target屬 性來判斷具體是哪個<li>被點擊了。下面咱們舉個簡單的例子來講明:
1
2
3
4
5
6
7
8
9
10
11
12
|
var list = document.querySelector( 'ul' );
list.addEventListener( 'click' , function (event) {
var target = event.target;
while (target.tagName !== 'LI' ) {
target = target.parentNode;
if (target === list) return ;
}
// Do stuff here
});
|
這樣就好多了,咱們僅僅使用了一個上層的事件監聽器,而且咱們不須要在爲添加元素而考慮它的事件監聽問題。這個概念很簡單,可是很是有用。
可是我並不建議你在你的項目中使用上面的這個粗糙的實現。相反,使用一個事件代理的JavaScript庫是更好的選擇,好比 FT Lab的ftdomdelegate。若是你在使用jQuery,你能夠在調用.on()方法的時候,將一個選擇器做爲第二個參數的方式來輕鬆的實現事件代理。
1
2
3
4
5
|
// Not using event delegation
$( 'li' ).on( 'click' , function (){});
// Using event delegation
$( 'ul' ).on( 'click' , 'li' , function (){});
|
Demo: Delegate event listeners
load事件能夠在任何資源(包括被依賴的資源)被加載完成時被觸發,這些資源能夠是圖片,css,腳本,視頻,音頻等文件,也能夠是document或者window。
1
2
3
|
image.addEventListener( 'load' , function (event) {
image.classList.add( 'has-loaded' );
});
|
window.onbeforeunload
讓開發人員能夠在想用戶離開一個頁面的時候進行確認。這個在有些應用中很是有用,好比用戶不當心關閉瀏覽器的tab,咱們能夠要求用戶保存他的修改和數據,不然將會丟失他此次的操做。
1
2
3
4
5
|
window.onbeforeunload =
function
() {
if
(textarea.value != textarea.defaultValue) {
return
'Do you want to leave the page and discard changes?'
;
}
};
|
須要注意的是,對頁面添加onbeforeunload
處理會致使瀏覽器不對頁面進行緩存?,這樣會影響頁面的訪問響應時間。 同時,onbeforeunload
的處理函數必須是同步的(synchronous)。
在Financial Times中,咱們使用了一個簡單的event.preventDefault相關的技巧防止了Safari在滾動的時候出現的抖動。(手機端開發接觸的很少,因此可能有所誤解,若是錯誤,請了解的同窗提點一下。)
1
2
3
|
document.body.addEventListener(
'touchmove'
,
function
(event) {
event.preventDefault();
});
|
須要提醒的是這個操做同時也會阻礙正常的原生滾動條的功能(好比使用overflow:scroll)。爲了使得內部的子元素在須要的時候可以使用滾 動條的功能,咱們在支持滾動的元素上監聽這個事件,而且在事件對象上設置一個標識屬性。在回調函數中,在document這一層,咱們經過對這個擴展的 isScrollable標識屬性來判斷是否對觸摸事件阻止默認的滾動行爲。
1
2
3
4
5
6
7
8
9
10
|
// Lower down in the DOM we set a flag
scrollableElement.addEventListener(
'touchmove'
,
function
(event) {
event.isScrollable =
true
;
});
// Higher up the DOM we check for this flag to decide
// whether to let the browser handle the scroll
document.addEventListener(
'touchmove'
,
function
(event) {
if
(!event.isScrollable) event.preventDefault();
});
|
在IE8即一下的版本中,咱們是不能操做事件對象的。做爲一個變通方案,咱們將一些擴展的屬性設置在event.target節點對向上。
在一些複雜的響應式佈局中,對window對象監聽resize事件是很是經常使用的一個技巧。僅僅經過css來達到想要的佈局效果比較困難。不少時候,咱們須要使用JavaScript來計算並設置一個元素的大小。
1
2
3
|
window.addEventListener(
'resize'
,
function
() {
// update the layout
});
|
我推薦使用防抖動的回調函數來統一調整回調的頻率,從而防止佈局上極端抖動的狀況出現。
如今在項目中,咱們常用CSS來執行一些轉換和動畫的效果。有些時候,咱們仍是須要知道一個特定動畫的結束時間。
1
2
3
|
el.addEventListener(
'transitionEnd'
,
function
() {
// Do stuff
});
|
一些注意點:
animationiteration事件會在當前的動畫元素完成一個動畫迭代的時候被觸發。這個事件很是有用,特別是當咱們想在某個迭代完成後中止一個動畫,但又不是在動畫過程當中打斷它。
1
2
3
4
5
6
7
8
9
10
11
12
|
function
start() {
div.classList.add(
'spin'
);
}
function
stop() {
div.addEventListener(
'animationiteration'
, callback);
function
callback() {
div.classList.remove(
'spin'
);
div.removeEventListener(
'animationiteration'
, callback);
}
}
|
若是你感興趣的話,我在博客中有另外一篇關於animationiteration事件的文章。
當咱們的應用在加載資源的時候發生了錯誤,咱們不少時候須要去作點什麼,尤爲當用戶處於一個不穩定的網絡狀況下。Financial Times中,咱們使用error事件來監測文章中的某些圖片加載失敗,從而馬上隱藏它。因爲「DOM Leven 3 Event」規定從新定義了error事件再也不冒泡,咱們可使用以下的兩種方式來處理這個事件。
1
2
3
|
imageNode.addEventListener(
'error'
,
function
(event) {
image.style.display =
'none'
;
});
|
不幸的是,addEventListener並不能處理全部的狀況。個人同事Kornel給了我一個很好的例子,說明確保圖片加載錯誤回調函數被執行的惟一方式是使用讓人詬病內聯事件處理函數(inline event handlers)。
1
|
|
緣由是你不能肯定綁定error事件處理函數的代碼會在error事件發生以前被執行。而使用內聯處理函數意味着在標籤被解析而且請求圖片的時候,error監聽器也將並綁定。
從事件模型的成功上,咱們能夠學到不少。咱們能夠在咱們的項目中使用相似的解耦的概念。應用中的模塊能夠有很高的很複雜度,只要它的複雜度被封裝隱藏 在一套簡單的接口背後。不少前端框架(好比Backbone.js)都是重度基於事件的,使用發佈-訂閱(publish and subscribe)的方式來處理跨模塊間的通訊,這點跟DOM很是類似。
基於事件的架構是極好的。它提供給咱們一套很是簡單通用的接口,經過針對這套接口的開發,咱們能完成適應成千上 萬不一樣設備的應用。經過事件,設備們能準確地告訴咱們正在發生的事情以及發生的時間,讓咱們爲所欲爲地作出響應。咱們再也不顧慮場景背後具體發生的事情,而 是經過一個更高層次的抽象來寫出更加使人驚豔的應用。
特別感謝Kornel對這篇文章作出的精彩的技術審查。
原文連接: smashingmagazine 翻譯: 伯樂在線 - Owen Chen