JS事件模型——監聽函式、事件代理、事件傳播——20181116
1、監聽函式
瀏覽器的事件模型,就是通過監聽函式(listener)對事件做出反應。事件發生後,瀏覽器監聽到了這個事件,就會執行對應的監聽函式。這是事件驅動程式設計模式(event-driven)的主要程式設計方式。
JavaScript 有三種方法,可以為事件繫結監聽函式。
1.1 HTML 的 on- 屬性
HTML 語言允許在元素的屬性中,直接定義某些事件的監聽程式碼。
1.2 元素節點的事件屬性
元素節點物件的事件屬性,同樣可以指定監聽函式。
1.3 EventTarget.addEventListener()
所有 DOM 節點例項都有addEventListener
<!DOCTYPE html> <html> <head> </head> <body> <input type="button" id="btn" value="btn"> <p id="para">Hello</p> <div><p id="try">try</p></div> <!-- HTML 語言允許在元素的屬性中,直接定義某些事件的監聽程式碼。 --> <div onclick="console.log('觸發')">觸發</div> <script> //元素節點物件的事件屬性,同樣可以指定監聽函式。使用這個方法指定的監聽函式,也是隻會在冒泡階段觸發。 document.getElementById('try').onclick = function (event) { console.log('觸發事件'); }; //所有 DOM 節點例項都有addEventListener方法,用來為該節點定義事件的監聽函式。 function hello() { console.log('Hello world'); } var button = document.getElementById('btn'); button.addEventListener('mouseover', hello); var para = document.getElementById('para'); para.addEventListener('click', function () { console.log(this.nodeName); // "P" }, false); var event = new Event('click'); para.dispatchEvent(event); var canceled = !para.dispatchEvent(event); if (canceled) { console.log('事件取消'); } else { console.log('事件未取消'); } </script> </body> </html>
1.4 小結
上面三種方法,第一種“HTML 的 on- 屬性”,違反了 HTML 與 JavaScript 程式碼相分離的原則,將兩者寫在一起,不利於程式碼分工,因此不推薦使用。
第二種“元素節點的事件屬性”的缺點在於,同一個事件只能定義一個監聽函式,也就是說,如果定義兩次onclick
屬性,後一次定義會覆蓋前一次。因此,也不推薦使用。
第三種EventTarget.addEventListener
是推薦的指定監聽函式的方法。它有如下優點:
- 同一個事件可以新增多個監聽函式。
- 能夠指定在哪個階段(捕獲階段還是冒泡階段)觸發監聽函式。
- 除了 DOM 節點,其他物件(比如
window
XMLHttpRequest
等)也有這個介面,它等於是整個 JavaScript 統一的監聽函式介面。
2、事件的傳播
一個事件發生後,會在子元素和父元素之間傳播(propagation)。這種傳播分成三個階段。
- 第一階段:從
window
物件傳導到目標節點(上層傳到底層),稱為“捕獲階段”(capture phase)。 - 第二階段:在目標節點上觸發,稱為“目標階段”(target phase)。
- 第三階段:從目標節點傳導回
window
物件(從底層傳回上層),稱為“冒泡階段”(bubbling phase)。
這種三階段的傳播模型,使得同一個事件會在多個節點上觸發。
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div>
<p>點選</p>
</div>
<script>
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;
console.log(event.currentTarget.tagName,event.eventPhase);
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'
</script>
</body>
</html>
上面程式碼中,<div>
節點之中有一個<p>
節點。
如果對這兩個節點,都設定click
事件的監聽函式(每個節點的捕獲階段和監聽階段,各設定一個監聽函式),共計設定四個監聽函式。然後,對<p>
點選,click
事件會觸發四次。
上面程式碼表示,click
事件被觸發了四次:<div>
節點的捕獲階段和冒泡階段各1次,<p>
節點的目標階段觸發了2次。
- 捕獲階段:事件從
<div>
向<p>
傳播時,觸發<div>
的click
事件; - 目標階段:事件從
<div>
到達<p>
時,觸發<p>
的click
事件; - 冒泡階段:事件從
<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
。
說明:Event.eventPhase
屬性返回一個整數常量,表示事件目前所處的階段。該屬性只讀。詳情見Event物件。
3、事件的代理
由於事件會在冒泡階段向上傳播到父節點,因此可以把子節點的監聽函式定義在父節點上,由父節點的監聽函式統一處理多個子元素的事件。這種方法叫做事件的代理(delegation)。
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<ul>
<li>點選1</li>
<li>點選2</li>
</ul>
<script>
var ul = document.querySelector('ul');
ul.addEventListener('click', function (event) {
if (event.target.tagName.toLowerCase() === 'li') {
// some code
console.log(1);
}
});
</script>
</body>
</html>
上面程式碼中,click
事件的監聽函式定義在<ul>
節點,但是實際上,它處理的是子節點<li>
的click
事件。這樣做的好處是,只要定義一個監聽函式,就能處理多個子節點的事件,而不用在每個<li>
節點上定義監聽函式。而且以後再新增子節點,監聽函式依然有效。
如果希望事件到某個節點為止,不再傳播,可以使用事件物件的stopPropagation
方法。
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div>
<ul>
<li id="dj1">點選1</li>
<li>點選2</li>
</ul>
</div>
<script>
var li = document.querySelector('li');
var div = document.querySelector('div');
li.addEventListener('click', function (event) {
if (event.target.id === 'dj1') {
// some code
console.log(111);
}
});
li.addEventListener('click', function (event) {
if (event.target.tagName.toLowerCase() === 'li') {
// some code
console.log(1);
}
});
div.addEventListener('click', function () {
console.log(222);
},true);//向下捕獲
// 事件傳播到 ul 元素後,就不再向下傳播了
ul.addEventListener('click', function (event) {
event.stopPropagation();
}, true);
// 事件冒泡到 ul 元素後,就不再向上冒泡了
ul.addEventListener('click', function (event) {
event.stopPropagation();
}, false);
</script>
</body>
</html>
上面程式碼中,stopPropagation
方法分別在捕獲階段和冒泡階段,阻止了事件的傳播。
但是,stopPropagation
方法只會阻止事件的傳播,不會阻止該事件觸發<p>
節點的其他click
事件的監聽函式。也就是說,不是徹底取消click
事件。
點選1時,由於該時間節點也添加了監聽函式,所以輸出結果有三個。
點選2時,由於
div.addEventListener('click', function () {
console.log(222);
},true);//向下捕獲
阻止了事件向下傳播,所以輸出結果只有一個。
如果想要徹底取消該事件,不再觸發後面所有click
的監聽函式,可以使用stopImmediatePropagation
方法。
p.addEventListener('click', function (event) {
event.stopImmediatePropagation();
console.log(1);
});
p.addEventListener('click', function(event) {
// 不會被觸發
console.log(2);
});
上面程式碼中,stopImmediatePropagation
方法可以徹底取消這個事件,使得後面繫結的所有click
監聽函式都不再觸發。所以,只會輸出1,不會輸出2。
————————————————————————————END——————————————————————————