在上一節中,學習和瞭解了DOM事件模型,瞭解到JavaScript中每種事件模型都有其自己獨具的特性。不同的事件模型中,綁定DOM事件的姿勢也將略有差異,在這一節中,我們一起來學習JavaScript中DOM事件是如何綁定的。
在JavaScript中,給DOM元素綁定事件主要分為兩大類:HTML中直接綁定和JavaScript中綁定。
HTML中直接綁定DOM事件
在HTML中綁定事件叫做內聯綁定事件。其使用方式非常的簡單,就是在HTML的元素中使用<event> 屬性來綁定事件,比如onclick 這樣的on(type) 屬性,其中type 指的就是DOM的事件(比如click ),它可以給這個DOM元素綁定一個類型的事件。比如,要為button 元素綁定一個click 事件,那麼就可以像下面這樣使用:
<!-- HTML -->
<button onclick="show();">Click Me</button>
<script>
function show() {
console.log('Show Me!')
}
</script>
當用戶在按鈕上單擊鼠標時,onclick 中的代碼將會運行,在上面的示例中,將會調用show() 函數。
這種方式對應的也是DOM Level0 模型中的事件綁定方式。雖然這種方式也能正常的DOM事件綁定方式,但這種方法是非常不鼓勵的。因為它是一種非常不靈活的事件綁定方式,它將HTML結構和JavaScript混合在一起。
JavaScript中綁定DOM事件
在JavaScript中綁定DOM事件有兩種方法:
element.on(type) = listener
element.addEventListener(type, listener, useCapture)
而這兩種方法用DOM事件模型來區分,或者劃分的話,又被劃分為:
接下來,咱們看看這兩咱方法的使用。
element.on(type) = listener
這種事件綁定方式和前面介紹的,在HTML中使用onclick=listener 綁定DOM事件有點類似,不同的是,前者在HTML中綁定,而這種方式是從HTML中分離出來。咱們將上面的示例改一下,就會像下面這樣:
<!-- HTML -->
<button>Click Me!</button>
// JavaScript
function show() {
console.log("Show Me!");
}
let btn = document.querySelector('button')
btn.onclick = show;
這個時候你用鼠標點擊按鈕時,同樣在控制台中能看到像下圖這樣的信息:
這裡有一個細節需要注意,在onclick 調用事件是,應該是show ,而不是show() 。如果使用的是btn.onclick=show() ,那麼show() 將是函數執行的結果,因此最後一個代碼中的onclick 就沒有定義(函數什麼也沒有返回)。這樣是行不通的。
但我們在HTML中這樣調用是可以執行的,前面的示例也向大家演示了。如果你實在想調用show() 函數,那麼前面的示例,你可以修改成這樣:
function show() {
console.log("Show Me!");
}
let btn = document.querySelector('button')
btn.onclick = function () {
show()
};
細想一下,其實這和在HTML中內聯綁定函數是一樣的,同樣是給DOM的元素onclick 屬性賦值一個函數,而他們的區別是:函數中的this 指向當前元素(內聯),而後面這種方式是在JavaScript中做的。另外一個區別就是內聯方式賦值的是一段JavaScript字符串,而這裡賦值的是一個函數,它可以接愛以一個參數event ,這個參數是點擊的事件對象。
<!-- HTML -->
<button onclick="show(this)">DOM Level0:Click Me!</button>
<button id="btn">DOM Level1:Click Me!</button>
// JavaScript
function show(e) {
console.log(e)
}
let btn = document.getElementById('btn')
btn.onclick = show;
比如,上面的示例,你分別點擊兩個按鈕,在控制台上輸入的結果將會是像下圖這樣:
另外,用賦值綁定函數也有一個缺點,那就是它只能綁定一次:
<!-- HTML -->
<button onclick="show(this);show(this);">DOM Level0:Click Me</button>
<button id="btn">DOM Level1:Click Me</button>
// JavaScript
function show(e) {
console.log(e);
}
let btn = document.getElementById('btn')
btn.onclick = show;
btn.onclick = show;
這個時候點擊按鈕的結果如下:
這種方法將我們的JavaScript和HTML分開。而且這種方式具有其自己的特徵:
- 它的本質就是給HTML元素添加相應的屬性
- 它的事件處理程序(綁定的事件)在執行時,其中的
this 指向當前的元素
- 該方式不會做同一元素的同類型事件綁定累加。也就是當你在同一個元素上多次綁定相同類型的監聽函數時,後者會覆蓋前者
element.addEventListener(type, listener, useCapture)
在編寫DOM腳本時能最大程度的控制事件,我們希望使用DOM Level2事件監聽器。它的語法是這樣子的:
element.addEventListener(type, listener[, useCapture]);
具體意思是:
element :表示要監聽事件的目標對象,可以是一個文檔上的元素Document 本身,window 或者XMLHttpRequest
type :表示事件類型的字符串,比如click 、change 、touchstart 等
listener :當指定的事件類型發生時被對知到的一個對象。該參數必是實現EventListener 接口的一個對象或函數,比如前面示例中的show() 函數
useCapture :設置事件的捕獲或者冒泡,它有兩個值,其中true 表示事件捕獲,為false 是事件冒泡,默認值為false
我們可以使用element.addEventListener(type, listener[, useCapture]); 來修改前面的示例:
<!-- HTML -->
<button>Click Me!</button>
// JavaScript
function show () {
console.log("Show Me!")
}
let btn = document.querySelector('button')
btn.addEventListener('click', show, false)
這個時候點擊按鈕,瀏覽器控制台輸出的結果如下圖所示:
這種DOM事件綁定的方式看起來比前面的方法要複雜一些,事實上這種複雜也就是額外的花了一些時間輸入代碼。addEventListener 給DOM元素綁定事件的一大優勢在於可以根據需要為事件提供儘可能多的處理程序(監聽函數)。你還可以指定在事件捕獲或理件冒泡(addEventListener 中的第三個參數)。
另外,在使用addEventListener 給DOM綁定事件時,其中第二個參數,即監聽函數,這個函數中的this 指向當前的DOM元素,同樣,函數也接受一個event 參數。比如上面的示例,咱們修改之後:
<!-- HTML -->
<button id="btn">Click Me</button>
// JavaScript
function show (e) {
console.log(this)
console.log(e)
}
let btn = document.getElementById('btn')
btn.addEventListener('click', show, false)
這個時候,你在瀏覽器中點擊按鈕,瀏覽器控制器將會輸出的結果像下面這樣:
使用addEventListener 來給DOM元素綁定事件,還有一個優勢,它可以給同一個DOM元素綁定多個函數,比如:
<!-- HTML -->
<button id="btn">Click Me!</button>
// JavaScript
function foo () {
console.log('Show foo function')
}
function bar () {
console.log('Show bar function')
}
let btn = document.getElementById('btn')
btn.addEventListener('click', foo)
btn.addEventListener('click', bar)
這個時候,點擊按鈕,瀏覽器控制台輸出的結果像下面這樣:
從結果中我們可以看出,給btn 元素綁定的click 事件,兩個函數都被執行了,並且執行順序按照綁定的順序執行。
前面也提到過,addEventListener 的第三個參數,如果給其添加第三個參數時,來看看有何不同。咱們在上面的示例中稍作修改:
<!-- HTML -->
<button id="btn">Click Me</button>
// JavaScript
function foo () {
console.log("Show foo function")
}
function bar () {
console.log("Show bar function")
}
let btn = document.getElementById('btn')
// foo: true; bar: true
console.log('==== foo: true; bar: true ====')
btn.addEventListener('click', foo, true) // => Show foo function
btn.addEventListener('click', bar, true) // => Show bar function
// foo: true; bar: false
console.log('==== foo: true; bar: false ====')
btn.addEventListener('click', foo, true) // => Show foo function
btn.addEventListener('click', bar, false) // => Show bar function
// foo: false; bar: true
console.log('==== foo: false; bar: true ====')
btn.addEventListener('click', foo, false) // => Show foo function
btn.addEventListener('click', bar, true) // => Show bar function
// foo: false; bar: false
console.log('==== foo: false; bar: false ====')
btn.addEventListener('click', foo, false) // => Show foo function
btn.addEventListener('click', bar, false) // => Show bar function
從瀏覽器控制台輸出的結果可以看出,不管useCapture 設置的是true 還是flase ,對於輸出的結果都是一樣的。這也說明,使用addEventListener 綁定的事件執行順序只和綁定順序有關,和useCapture 並無關。
也就是說:
使用addEventListener 可以給同一個DOM元素綁定多個函數,並且它的執行順序將按照綁定的順序執行!
上面我們看到的是給同一個DOM元素綁定的是不同的監聽函數,如果我們給同一個DOM元素多次綁定同一個函數:
<!-- HTML -->
<button id="btn">Click Me!</button>
// JavaScript
function show () {
console.log(this)
}
let btn = document.getElementById('btn')
btn.addEventListener('click', show)
btn.addEventListener('click', show)
輸出的結果如下:
從結果上可以看出,我們雖然在同一個DOM元素上綁定了兩次click 事件,而且監聽函數都是show ,但我們輸出的結果卻只有一個,如上圖所示。這個時候addEventListener 的第三個參數,都是默認值false 。
我們再把addEventListener 修改一下:
btn.addEventListener('click', show, true)
btn.addEventListener('click', show, false)
輸出的結果如下:
當你點擊按鈕時,show() 函數被執行了兩次。這個時候addEventListener 的第三個參數分別是true 和false 。再測試一下,如果第三個參數都是true 呢?結果如下:
和同樣為false 一樣,雖然綁定了兩次,但只輸出一個結果。最後再看一種情形:
btn.addEventListener('click', show, false)
btn.addEventListener('click', show, true)
輸出的結果也是兩次。通過這幾個簡單的示列,我們可以得到的結果是:
使用addEventListener 可以給一個DOM元素綁定同一個函數,最多只能綁定useCapture 類型不同的兩次!
簡單的歸納一下addEventListener 給DOM元素綁定事件的一些特徵:
- 當用戶進行一個操作時,瀏覽器會根據該操作依次觸發相應的事件監聽函數,此時我們暫時不會考慮你程序中的人為阻止,比如
stopPropagation 、preventDefault 等操作
- 當同一個元素上綁定了多次同類型事件,比如
button 元素上做了多次click 事件,那麼遵循“先綁定先觸發”的原則,而且最多只能綁定useCapture 類型不同的兩次
- 它的事件處理程序在執行時,其中
this 指向當前的元素
事件對象
從前面的示例中,我們可以看到,DOM事件調用處理程序時,可以給這個處理程序傳一個參數,比如event 參數。事實上,當事件發生時,瀏覽器會創建一個事件對象,將詳細信息放入這個對象當中,並將其作為參數傳遞給處理程序。比如下面這個示例:
<!-- HTML -->
<button id="btn">Click Me!</button>
// JavaScript
function show(event) {
console.log(event)
}
let btn = document.getElementById('btn')
btn.addEventListener('click', show)
當你在瀏覽器中點擊按鈕時,瀏覽器控制台中就會輸出這個Event 對象的所有信息,如下圖所示:
Event 對象在event 第一次觸發的時候被創建出來,並且一直伴隨著事件在DOM結構中流轉的整個生命週期。event 對象會被作為第一個參數傳遞給事件監聽的回調函數。我們可以通過這個event 對象來獲取到大量當前事件相關的信息:
type (String) :事件的名稱
target (node) :事件起源的DOM節點
currentTarget?(node) :當前回調函數被觸發的DOM節點(後面會做比較詳細的介紹)
bubbles (boolean) :指明這個事件是否是一個冒泡事件(接下來會做解釋)
preventDefault(function) :這個方法將阻止瀏覽器中用戶代理對當前事件的相關默認行為被觸發。比如阻止<a> 元素的click 事件加載一個新的頁面
stopPropagation (function) :這個方法將阻止當前事件鏈上後面的元素的回調函數被觸發,當前節點上針對此事件的其他回調函數依然會被觸發。(我們稍後會詳細介紹。)
stopImmediatePropagation (function) :這個方法將阻止當前事件鏈上所有的回調函數被觸發,也包括當前節點上針對此事件已綁定的其他回調函數。
cancelable (boolean) :這個變量指明這個事件的默認行為是否可以通過調用event.preventDefault 來阻止。也就是說,只有cancelable 為true 的時候,調用event.preventDefault 才能生效。
defaultPrevented (boolean) :這個狀態變量表明當前事件對象的preventDefault 方法是否被調用過
isTrusted (boolean) :如果一個事件是由設備本身(如瀏覽器)觸發的,而不是通過JavaScript模擬合成的,那個這個事件被稱為可信任的(trusted)
eventPhase (number) :這個數字變量表示當前這個事件所處的階段(phase):none(0) , capture(1) ,target(2) ,bubbling(3) 。我們會在下一個部分介紹事件的各個階段
timestamp (number) :事件發生的時間
此外事件對象還可能擁有很多其他的屬性,但是他們都是針對特定的event 的。比如,鼠標事件包含clientX 和clientY 屬性來表明鼠標在當前視窗的位置。比如像下面這個示例:
<!-- HTML -->
<button id="btn">Click Me!</button>
// JavaScript
function show(event) {
console.log(`${event.type} at ${event.currentTarget}`)
console.log(`Coordinates: (${event.clientX}, ${event.clientY})`)
}
let btn = document.getElementById('btn')
btn.addEventListener('click', show)
當你在button 不同的位置點擊按鈕時,瀏覽器控制台將輸出不同的結果,特別是鼠標坐標:
對象處理程序:handleEvent
我們可以使用addEventListener 將對象指定為事件處理程序。當一個事件發生時,它的handleEvent 方法也隨之被調用。
比如,將上面的示例做一下修改:
btn.addEventListener('click', {
handleEvent(event) {
console.log(`${event.type} at ${event.currentTarget}`)
console.log(`Coordinates: (${event.clientX}, ${event.clientY})`)
}
})
這裡輸出的結果和上面示例輸出的結果是相似的:
換句話說,當addEventListener 接收到一個對象作為處理程序時,它在發生事件時調用object. handleevent (event) 。
我們也可以用一個類:
class Show {
handleEvent(event) {
switch(event.type) {
case 'mousedown':
btn.innerHTML = 'Mouse button pressed';
break;
case 'mouseup':
btn.innerHTML = '...and released.';
break
}
}
}
let show = new Show();
btn.addEventListener('mousedown', show)
btn.addEventListener('mouseup', show)
當你在按鈕上按下鼠標和鬆開鼠標時,按鈕的文本內容將會變化:
在這裡,同一個對象處理這兩個事件。請注意,我們需要顯式地設置事件來使用addEventListener 監聽。Show 對象只在這裡得到musedown 和mouseup ,而不是任何其他類型的事件。
handleEvent 方法不需要單獨完成所有工作。它可以調用其他特定於事件的方法,像下面這樣:
class Show {
handleEvent(event) {
// mousedown -> onMousedown
let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
this[method](event);
}
onMousedown() {
btn.innerHTML = "Mouse button pressed";
}
onMouseup() {
btn.innerHTML += "...and released.";
}
}
let show = new Show();
btn.addEventListener('mousedown', show);
btn.addEventListener('mouseup', show);
現在事件處理程序顯然是分開的,這可能更容易支持。
事件階段
通過上面的學習,我們知道在JavaScript中怎麼給DOM元素綁定事件。而且在DOM Level2的DOM模型中,給DOM元素綁定事件會經歷三個階段:捕獲階段、處於目標階段和冒泡階段。而addEventListener 的第三個參數useCapture 就是用來指定該事件監聽函數是捕獲階段還是冒泡階段被觸發。
所以我們現在要瞭解一些概念,瞭解它們是如何運作?
簡而言之:事件一開始從文檔的根節點流向目標對象(捕獲階段),然後在目標對向上被觸發(目標階段),之後再回溯到文檔的根節點(冒泡階段)。
事件階段中的事件捕獲階段、目標階段和事件冒泡階段是整個事件流的三個重要概念,也是理解JavaScript中DOM事件的重要概念,在這裡只先簡單的闡述其概念,有關於更細的介紹,將在此系列的後續文章中闡述。
事件捕獲階段
事件的第一個階段是捕獲階段。事件從文檔的根節點出發,隨著DOM樹的結構向事件的目標節點流去。途中經過各個層次的DOM節點,並在各節點上觸發捕獲事件,直到到達事件的目標節點。捕獲階段的主要任務是建立傳播路徑,在冒泡階段,事件會通過這個路徑回溯到文檔跟節點。
我們可以通過將addEventListener 的第三個參數設置成true 來為事件的捕獲階段添加監聽回調函數。在實際應用中,我們並沒有太多使用捕獲階段監聽的用例,但是通過在捕獲階段對事件的處理,我們可以阻止類似click 事件在某個特定元素上被觸發。
btn.addEventListener('click', function(event) {
event.stopPropagation();
}, true);
如果你對這種用法不是很瞭解的話,最好還是將useCapture 設置為false 或者undefined ,從而在冒泡階段對事件進行監聽。
目標階段
當事件到達目標節點的,事件就進入了目標階段。事件在目標節點上被觸發,然後會逆向回流,直到傳播至最外層的文檔節點。
對於多層嵌套的節點,鼠標和指針事件經常會被定位到最裡層的元素上。假設,你在一個<div> 元素上設置了click 事件的監聽函數,而用戶點擊在了這個<div> 元素內部的<p> 元素上,那麼<p> 元素就是這個事件的目標元素。事件冒泡讓我們可以在這個<div> (或者更上層的)元素上監聽click 事件,並且事件傳播過程中觸發回調函數。
冒泡階段
事件在目標元素上觸發後,並不在這個元素上終止。它會隨著DOM樹一層層向上冒泡,直到到達最外層的根節點。也就是說,同一個事件會依次在目標節點的父節點,父節點的父節點。。。直到最外層的節點上被觸發。
將DOM結構想像成一個洋蔥,事件目標是這個洋蔥的中心。在捕獲階段,事件從最外層鑽入洋蔥,穿過途徑的每一層。在到達中心後,事件被觸發(目標階段)。然後事件開始回溯,再次經過每一層返回(冒泡階段)。當到達洋蔥表面的時候,這次旅程就結束了。
冒泡過程非常有用。它將我們從對特定元素的事件監聽中釋放出來,相反,我們可以監聽DOM樹上更上層的元素,等待事件冒泡的到達。如果沒有事件冒泡,在某些情況下,我們需要監聽很多不同的元素來確保捕獲到想要的事件。
絕大多數事件會冒泡,但並非所有的。當你發現有些事件不冒泡的時候,它肯定是有原因的。不相信?你可以查看一下相應的規範說明。
該使用哪種方式
上面介紹了三種方式來給 DOM元素綁定事件。但你不應該使用HTML事件處理程序屬性,因為這些屬性已經過時了,而且也是不好的做法,前面有介紹過。
另外兩種是相對可互換的,到少對於簡單的用途:
- 事件處理程序屬性功能和選會更少,但是具有更好的跨瀏覽器兼容性
- DOM Level2 事件(
addEventListener )更強大,但也可以變得更加複雜,並且支持不足(IE9以下不支持)。
addEventListener 的主要優點是,如果需要的話,可以使用removeEventListener 刪除事件處理程序代碼,而且如果有需要,可以向同一類型的元素添加多個監聽器。例如,你可以在一個元素上多次調用addEventListener('click', function() { ... }) ,並可在第二個參數中指定不同的函數。對於事件處理程序屬性來說,這是不可能的,因為後面任何設置的屬性都會嘗試覆蓋較早的屬性,例如:
element.onclick = function1;
element.onclick = function2;
總結
在JavaScript中給DOM綁定事件有三種姿勢:
- HTML屬性:
onclick="..."
- DOM屬性:
element.onclick = function
- 方法:
element.addEventListener(type, listener[, useCapture]); 綁定事件;removeEventListener 刪除事件
HTML屬性很少使用,因為HTML標記中使用JavaScript代碼,看起來有點怪,而且耦合在一起,也不能寫很多代碼。
DOM屬性可以使用,但是我們不能為特定事件分配多個處理程序(事件監聽函數)。在許多情況下,這處限制並不緊迫。
最後一種方式是最靈活的,但也是最長的書寫方式。很少有事件只適用於它,比如transtioned 和DOMContentLoaded 。addEventListener 還支持對象作為事件處理程序。在這種情況下,在事件發生時調用handleEvent 方法。
無論你使用哪種方式給DOM元素綁定事件,它都會得到一個事件對象作為第一參數。該對象包含發生了什麼事情的詳細信息。
另外外們可以在同一個元素上綁定多個事件,其實我們也可以在多個元素上綁定同一個事件,下一節我們將來一起探討如何在多個元素上綁定同一個事件。
|