# 事件模型
## 監聽函數
瀏覽器的事件模型,就是通過監聽函數(listener)對事件做出反應。事件發生后,瀏覽器監聽到了這個事件,就會執行對應的監聽函數。這是事件驅動編程模式(event-driven)的主要編程方式。
JavaScript 有三種方法,可以為事件綁定監聽函數。
### HTML 的 on- 屬性
HTML 語言允許在元素的屬性中,直接定義某些事件的監聽代碼。
```html
<body onload="doSomething()">
<div onclick="console.log('觸發事件')">
```
上面代碼為`body`節點的`load`事件、`div`節點的`click`事件,指定了監聽代碼。一旦事件發生,就會執行這段代碼。
元素的事件監聽屬性,都是`on`加上事件名,比如`onload`就是`on + load`,表示`load`事件的監聽代碼。
注意,這些屬性的值是將會執行的代碼,而不是一個函數。
```html
<!-- 正確 -->
<body onload="doSomething()">
<!-- 錯誤 -->
<body onload="doSomething">
```
一旦指定的事件發生,`on-`屬性的值是原樣傳入 JavaScript 引擎執行。因此如果要執行函數,不要忘記加上一對圓括號。
使用這個方法指定的監聽代碼,只會在冒泡階段觸發。
```html
<div onclick="console.log(2)">
<button onclick="console.log(1)">點擊</button>
</div>
```
上面代碼中,`<button>`是`<div>`的子元素。`<button>`的`click`事件,也會觸發`<div>`的`click`事件。由于`on-`屬性的監聽代碼,只在冒泡階段觸發,所以點擊結果是先輸出`1`,再輸出`2`,即事件從子元素開始冒泡到父元素。
直接設置`on-`屬性,與通過元素節點的`setAttribute`方法設置`on-`屬性,效果是一樣的。
```javascript
el.setAttribute('onclick', 'doSomething()');
// 等同于
// <Element onclick="doSomething()">
```
### 元素節點的事件屬性
元素節點對象的事件屬性,同樣可以指定監聽函數。
```javascript
window.onload = doSomething;
div.onclick = function (event) {
console.log('觸發事件');
};
```
使用這個方法指定的監聽函數,也是只會在冒泡階段觸發。
注意,這種方法與 HTML 的`on-`屬性的差異是,它的值是函數名(`doSomething`),而不像后者,必須給出完整的監聽代碼(`doSomething()`)。
### EventTarget.addEventListener()
所有 DOM 節點實例都有`addEventListener`方法,用來為該節點定義事件的監聽函數。
```javascript
window.addEventListener('load', doSomething, false);
```
`addEventListener`方法的詳細介紹,參見`EventTarget`章節。
### 小結
上面三種方法,第一種“HTML 的 on- 屬性”,違反了 HTML 與 JavaScript 代碼相分離的原則,將兩者寫在一起,不利于代碼分工,因此不推薦使用。
第二種“元素節點的事件屬性”的缺點在于,同一個事件只能定義一個監聽函數,也就是說,如果定義兩次`onclick`屬性,后一次定義會覆蓋前一次。因此,也不推薦使用。
第三種`EventTarget.addEventListener`是推薦的指定監聽函數的方法。它有如下優點:
- 同一個事件可以添加多個監聽函數。
- 能夠指定在哪個階段(捕獲階段還是冒泡階段)觸發監聽函數。
- 除了 DOM 節點,其他對象(比如`window`、`XMLHttpRequest`等)也有這個接口,它等于是整個 JavaScript 統一的監聽函數接口。
## this 的指向
監聽函數內部的`this`指向觸發事件的那個元素節點。
```html
<button id="btn" onclick="console.log(this.id)">點擊</button>
```
執行上面代碼,點擊后會輸出`btn`。
其他兩種監聽函數的寫法,`this`的指向也是如此。
```javascript
// HTML 代碼如下
// <button id="btn">點擊</button>
var btn = document.getElementById('btn');
// 寫法一
btn.onclick = function () {
console.log(this.id);
};
// 寫法二
btn.addEventListener(
'click',
function (e) {
console.log(this.id);
},
false
);
```
上面兩種寫法,點擊按鈕以后也是輸出`btn`。
## 事件的傳播
一個事件發生后,會在子元素和父元素之間傳播(propagation)。這種傳播分成三個階段。
- **第一階段**:從`window`對象傳導到目標節點(上層傳到底層),稱為“捕獲階段”(capture phase)。
- **第二階段**:在目標節點上觸發,稱為“目標階段”(target phase)。
- **第三階段**:從目標節點傳導回`window`對象(從底層傳回上層),稱為“冒泡階段”(bubbling phase)。
這種三階段的傳播模型,使得同一個事件會在多個節點上觸發。
```html
<div>
<p>點擊</p>
</div>
```
上面代碼中,`<div>`節點之中有一個`<p>`節點。
如果對這兩個節點,都設置`click`事件的監聽函數(每個節點的捕獲階段和冒泡階段,各設置一個監聽函數),共計設置四個監聽函數。然后,對`<p>`點擊,`click`事件會觸發四次。
```javascript
var phases = {
1: 'capture',
2: 'target',
3: 'bubble'
};
var div = document.querySelector('div');
var p = document.querySelector('p');
div.addEventListener('click', callback, true);
p.addEventListener('click', callback, true);
div.addEventListener('click', callback, false);
p.addEventListener('click', callback, false);
function callback(event) {
var tag = event.currentTarget.tagName;
var phase = phases[event.eventPhase];
console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
}
// 點擊以后的結果
// Tag: 'DIV'. EventPhase: 'capture'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'DIV'. EventPhase: 'bubble'
```
上面代碼表示,`click`事件被觸發了四次:`<div>`節點的捕獲階段和冒泡階段各1次,`<p>`節點的目標階段觸發了2次。
1. 捕獲階段:事件從`<div>`向`<p>`傳播時,觸發`<div>`的`click`事件;
2. 目標階段:事件從`<div>`到達`<p>`時,觸發`<p>`的`click`事件;
3. 冒泡階段:事件從`<p>`傳回`<div>`時,再次觸發`<div>`的`click`事件。
其中,`<p>`節點有兩個監聽函數(`addEventListener`方法第三個參數的不同,會導致綁定兩個監聽函數),因此它們都會因為`click`事件觸發一次。所以,`<p>`會在`target`階段有兩次輸出。
注意,瀏覽器總是假定`click`事件的目標節點,就是點擊位置嵌套最深的那個節點(本例是`<div>`節點里面的`<p>`節點)。所以,`<p>`節點的捕獲階段和冒泡階段,都會顯示為`target`階段。
事件傳播的最上層對象是`window`,接著依次是`document`,`html`(`document.documentElement`)和`body`(`document.body`)。也就是說,上例的事件傳播順序,在捕獲階段依次為`window`、`document`、`html`、`body`、`div`、`p`,在冒泡階段依次為`p`、`div`、`body`、`html`、`document`、`window`。
## 事件的代理
由于事件會在冒泡階段向上傳播到父節點,因此可以把子節點的監聽函數定義在父節點上,由父節點的監聽函數統一處理多個子元素的事件。這種方法叫做事件的代理(delegation)。
```javascript
var ul = document.querySelector('ul');
ul.addEventListener('click', function (event) {
if (event.target.tagName.toLowerCase() === 'li') {
// some code
}
});
```
上面代碼中,`click`事件的監聽函數定義在`<ul>`節點,但是實際上,它處理的是子節點`<li>`的`click`事件。這樣做的好處是,只要定義一個監聽函數,就能處理多個子節點的事件,而不用在每個`<li>`節點上定義監聽函數。而且以后再添加子節點,監聽函數依然有效。
如果希望事件到某個節點為止,不再傳播,可以使用事件對象的`stopPropagation`方法。
```javascript
// 事件傳播到 p 元素后,就不再向下傳播了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, true);
// 事件冒泡到 p 元素后,就不再向上冒泡了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, false);
```
上面代碼中,`stopPropagation`方法分別在捕獲階段和冒泡階段,阻止了事件的傳播。
但是,`stopPropagation`方法只會阻止事件的傳播,不會阻止該事件觸發`<p>`節點的其他`click`事件的監聽函數。也就是說,不是徹底取消`click`事件。
```javascript
p.addEventListener('click', function (event) {
event.stopPropagation();
console.log(1);
});
p.addEventListener('click', function(event) {
// 會觸發
console.log(2);
});
```
上面代碼中,`p`元素綁定了兩個`click`事件的監聽函數。`stopPropagation`方法只能阻止這個事件的傳播,不能取消這個事件,因此,第二個監聽函數會觸發。輸出結果會先是1,然后是2。
如果想要徹底取消該事件,不再觸發后面所有`click`的監聽函數,可以使用`stopImmediatePropagation`方法。
```javascript
p.addEventListener('click', function (event) {
event.stopImmediatePropagation();
console.log(1);
});
p.addEventListener('click', function(event) {
// 不會被觸發
console.log(2);
});
```
上面代碼中,`stopImmediatePropagation`方法可以徹底取消這個事件,使得后面綁定的所有`click`監聽函數都不再觸發。所以,只會輸出1,不會輸出2。
- 前言
- 入門篇
- 導論
- 歷史
- 基本語法
- 數據類型
- 概述
- null,undefined 和布爾值
- 數值
- 字符串
- 對象
- 函數
- 數組
- 運算符
- 算術運算符
- 比較運算符
- 布爾運算符
- 二進制位運算符
- 其他運算符,運算順序
- 語法專題
- 數據類型的轉換
- 錯誤處理機制
- 編程風格
- console 對象與控制臺
- 標準庫
- Object 對象
- 屬性描述對象
- Array 對象
- 包裝對象
- Boolean 對象
- Number 對象
- String 對象
- Math 對象
- Date 對象
- RegExp 對象
- JSON 對象
- 面向對象編程
- 實例對象與 new 命令
- this 關鍵字
- 對象的繼承
- Object 對象的相關方法
- 嚴格模式
- 異步操作
- 概述
- 定時器
- Promise 對象
- DOM
- 概述
- Node 接口
- NodeList 接口,HTMLCollection 接口
- ParentNode 接口,ChildNode 接口
- Document 節點
- Element 節點
- 屬性的操作
- Text 節點和 DocumentFragment 節點
- CSS 操作
- Mutation Observer API
- 事件
- EventTarget 接口
- 事件模型
- Event 對象
- 鼠標事件
- 鍵盤事件
- 進度事件
- 表單事件
- 觸摸事件
- 拖拉事件
- 其他常見事件
- GlobalEventHandlers 接口
- 瀏覽器模型
- 瀏覽器模型概述
- window 對象
- Navigator 對象,Screen 對象
- Cookie
- XMLHttpRequest 對象
- 同源限制
- CORS 通信
- Storage 接口
- History 對象
- Location 對象,URL 對象,URLSearchParams 對象
- ArrayBuffer 對象,Blob 對象
- File 對象,FileList 對象,FileReader 對象
- 表單,FormData 對象
- IndexedDB API
- Web Worker
- 附錄:網頁元素接口
- a
- img
- form
- input
- button
- option
- video,audio