JavaScript核心之事件詳解(EventTarget接口,js事件傳播,Event對象)


事件是一種異步編程的實現方式,本質上是程序各個組成部分之間傳遞的特定消息。DOM支持大量的事件,本節介紹DOM的事件編程。

1 EventTarget接口
DOM的事件操作(監聽和觸發),都定義在EventTarget接口。Element節點、document節點和window對象,都部署了這個接口。此外,XMLHttpRequest、AudioNode、AudioContext等瀏覽器內置對象,也部署了這個接口。該接口就是三個方法,addEventListener和removeEventListener用於綁定和移除監聽函數,dispatchEvent用於觸發事件。

1.1 addEventListener()
addEventListener方法用於在當前節點或對象上,定義一個特定事件的監聽函數。

target.addEventListener(type, listener[, useCapture]);
上面是使用格式,addEventListener方法接受三個參數。

type,事件名稱,大小寫不敏感。注意:事件名稱是沒有"on"開頭的
listener,監聽函數。指定事件發生時,會調用該監聽函數。
useCapture,監聽函數是否在捕獲階段(capture)觸發。該參數是一個布爾值,默認為false(表示監聽函數只在冒泡階段被觸發)。老式瀏覽器規定該參數必寫,較新版本的瀏覽器允許該參數可選。為了保持兼容,建議總是寫上該參數。
如圖是捕獲/冒泡過程:

 

下面是一個例子。

function hello(){
console.log('Hello world');
}

var button = document.getElementById("btn");
button.addEventListener('click', hello, false);
上面代碼中,addEventListener方法為button節點,綁定click事件的監聽函數hello,該函數只在冒泡階段觸發。

可以使用addEventListener方法,為當前對象的同一個事件,添加多個監聽函數。這些函數按照添加順序觸發,即先添加先觸發。如果為同一個事件多次添加同一個監聽函數,該函數只會執行一次,多余的添加將自動被去除(不必使用removeEventListener方法手動去除)。

function hello(){
console.log('Hello world');
}

document.addEventListener('click', hello, false);
document.addEventListener('click', hello, false);
執行上面代碼,點擊文檔只會輸出一行“Hello world”。

如果希望向監聽函數傳遞參數,可以用匿名函數包裝一下監聽函數。

function print(x) {
console.log(x);
}

var el = document.getElementById("div1");
el.addEventListener("click", function(){print('Hello')}, false);
上面代碼通過匿名函數,向監聽函數print傳遞了一個參數。

1.1.1 addEventListenerthis對象的指向
實際編程中,監聽函數內部的this對象,常常需要指向觸發事件的那個Element節點。

addEventListener方法指定的監聽函數,內部的this對象總是指向觸發事件的那個節點。

// HTML代碼為
// <p id="para">Hello</p>

var id = 'doc';
var para = document.getElementById('para');

function hello(){
console.log(this.id);
}

para.addEventListener('click', hello, false);
執行上面代碼,點擊p節點會輸出para。這是因為監聽函數被“拷貝”成了節點的一個屬性,使用下面的寫法,會看得更清楚。

para.onclick = hello;
如果將監聽函數部署在Element節點的on-屬性上面,this不會指向觸發事件的元素節點。

<p id="para" onclick="hello()">Hello</p>
<!-- 或者使用JavaScript代碼 -->
<script>
pElement.setAttribute('onclick', 'hello()');
</script>
執行上面代碼,點擊p節點會輸出doc。這是因為這里只是調用hello函數,而hello函數實際是在全局作用域執行,相當於下面的代碼。

para.onclick = function(){
hello();
}
一種解決方法是,不引入函數作用域,直接在on-屬性寫入所要執行的代碼。因為on-屬性是在當前節點上執行的。

<p id="para" onclick="console.log(id)">Hello</p>
<!-- 或者 -->
<p id="para" onclick="console.log(this.id)">Hello</p>
上面兩行,最后輸出的都是para。

總結一下,以下寫法的this對象都指向Element節點。

// JavaScript代碼
element.onclick = print
element.addEventListener('click', print, false)
element.onclick = function () {console.log(this.id);}

// HTML代碼
<element onclick="console.log(this.id)">
以下寫法的this對象,都指向全局對象。

// JavaScript代碼
element.onclick = function (){ doSomething() };
element.setAttribute('onclick', 'doSomething()');

// HTML代碼
<element onclick="doSomething()">
1.2 removeEventListener()
removeEventListener方法用來移除addEventListener方法添加的事件監聽函數。

div.addEventListener('click', listener, false);
div.removeEventListener('click', listener, false);
removeEventListener方法的參數,與addEventListener方法完全一致。它對第一個參數“事件類型”,也是大小寫不敏感。

注意,removeEventListener方法移除的監聽函數,必須與對應的addEventListener方法的參數完全一致,而且在同一個元素節點,否則無效。

1.3 dispatchEvent()
dispatchEvent()方法在當前節點上觸發指定事件,從而觸發監聽函數的執行。該方法返回一個布爾值,只要有一個監聽函數調用了Event.preventDefault(),則返回值為false,否則為true。

target.dispatchEvent(event)
dispatchEvent方法的參數是一個Event對象的實例。

para.addEventListener('click', hello, false);
var event = new Event('click');
para.dispatchEvent(event);
上面代碼在當前節點觸發了click事件。

如果dispatchEvent方法的參數為空,或者不是一個有效的事件對象,將報錯。

下面代碼根據dispatchEvent方法的返回值,判斷事件是否被取消了。

var canceled = !cb.dispatchEvent(event);
if (canceled) {
console.log('事件取消');
} else {
console.log('事件未取消');
}
}
2 監聽函數
監聽函數(listener)是事件發生時,程序所要執行的函數。它是事件驅動編程模式的主要編程方式。

DOM提供三種方法,可以用來為事件綁定監聽函數。

2.1 HTML標簽的on-屬性
HTML語言允許在元素標簽的屬性中,直接定義某些事件的監聽代碼。

<body onload="doSomething()">

<div onclick="console.log('觸發事件')">
上面代碼為body節點的load事件、div節點的click事件,指定了監聽函數。

使用這個方法指定的監聽函數,只會在冒泡階段觸發。

注意,使用這種方法時,on-屬性的值是“監聽代碼”,而不是“監聽函數”。也就是說,一旦指定事件發生,這些代碼是原樣傳入JavaScript引擎執行。因此如果要執行函數,必須在函數名后面加上一對圓括號。

另外,Element節點的setAttribue方法,其實設置的也是這種效果。

el.setAttribute('onclick', 'doSomething()');
2.2 Element節點的事件屬性
Element節點有事件屬性,可以定義監聽函數。

window.onload = doSomething;

div.onclick = function(event){
console.log('觸發事件');
};
使用這個方法指定的監聽函數,只會在冒泡階段觸發。

2.3 addEventListener方法
通過Element節點、document節點、window對象的addEventListener方法,也可以定義事件的監聽函數。

window.addEventListener('load', doSomething, false);
在上面三種方法中,第一種“HTML標簽的on-屬性”,違反了HTML與JavaScript代碼相分離的原則;第二種“Element節點的事件屬性”的缺點是,同一個事件只能定義一個監聽函數,也就是說,如果定義兩次onclick屬性,后一次定義會覆蓋前一次。因此,這兩種方法都不推薦使用,除非是為了程序的兼容問題,因為所有瀏覽器都支持這兩種方法。

addEventListener是推薦的指定監聽函數的方法。它有如下優點:

可以針對同一個事件,添加多個監聽函數。
能夠指定在哪個階段(捕獲階段還是冒泡階段)觸發回監聽函數。
除了DOM節點,還可以部署在window、XMLHttpRequest等對象上面,等於統一了整個JavaScript的監聽函數接口。
3 事件的傳播
3.1 傳播的三個階段
當一個事件發生以后,它會在不同的DOM節點之間傳播(propagation)。這種傳播分成三個階段:

第一階段:從window對象傳導到目標節點,稱為“捕獲階段”(capture phase)。
第二階段:在目標節點上觸發,稱為“目標階段”(target phase)。
第三階段:從目標節點傳導回window對象,稱為“冒泡階段”(bubbling phase)。
這種三階段的傳播模型,會使得一個事件在多個節點上觸發。比如,假設div節點之中嵌套一個p節點。

<div>
<p>Click Me</p>
</div>
如果對這兩個節點的click事件都設定監聽函數,則click事件會被觸發四次。

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事件被觸發了四次:p節點的捕獲階段和冒泡階段各1次,div節點的捕獲階段和冒泡階段各1次。

捕獲階段:事件從div向p傳播時,觸發div的click事件;
目標階段:事件從div到達p時,觸發p的click事件;
目標階段:事件離開p時,觸發p的click事件;
冒泡階段:事件從p傳回div時,再次觸發div的click事件。
注意,用戶點擊網頁的時候,瀏覽器總是假定click事件的目標節點,就是點擊位置的嵌套最深的那個節點(嵌套在div節點的p節點)。

事件傳播的最上層對象是window,接着依次是document,html(document.documentElement)和body(document.dody)。也就是說,如果body元素中有一個div元素,點擊該元素。事件的傳播順序,在捕獲階段依次為window、document、html、body、div,在冒泡階段依次為div、body、html、document、window。

3.2 事件的代理
由於事件會在冒泡階段向上傳播到父節點,因此可以把子節點的監聽函數定義在父節點上,由父節點的監聽函數統一處理多個子元素的事件。這種方法叫做事件的代理(delegation)。

var ul = document.querySelector('ul');

ul.addEventListener('click', function(event) {
if (event.target.tagName.toLowerCase() === 'li') {
// some code
}
});
上面代碼的click事件的監聽函數定義在ul節點,但是實際上,它處理的是子節點li的click事件。這樣做的好處是,只要定義一個監聽函數,就能處理多個子節點的事件,而且以后再添加子節點,監聽函數依然有效。

如果希望事件到某個節點為止,不再傳播,可以使用事件對象的stopPropagation方法。

p.addEventListener('click', function(event) {
event.stopPropagation();
});
使用上面的代碼以后,click事件在冒泡階段到達p節點以后,就不再向上(父節點的方向)傳播了。

但是,stopPropagation方法不會阻止p節點上的其他click事件的監聽函數。如果想要不再觸發那些監聽函數,可以使用stopImmediatePropagation方法。

p.addEventListener('click', function(event) {
event.stopImmediatePropagation();
});

p.addEventListener('click', function(event) {
// 不會被觸發
});
4 Event對象
事件發生以后,會生成一個事件對象,作為參數傳給監聽函數。瀏覽器原生提供一個Event對象,所有的事件都是這個對象的實例,或者說繼承了Event.prototype對象。

Event對象本身就是一個構造函數,可以用來生成新的實例。

event = new Event(typeArg, eventInit);
Event構造函數接受兩個參數。第一個參數是字符串,表示事件的名稱;第二個參數是一個對象,表示事件對象的配置。該參數可以有以下兩個屬性。

bubbles:布爾值,可選,默認為false,表示事件對象是否冒泡。
cancelable:布爾值,可選,默認為false,表示事件是否可以被取消。
var ev = new Event("look", {"bubbles":true, "cancelable":false});
document.dispatchEvent(ev);
上面代碼新建一個look事件實例,然后使用dispatchEvent方法觸發該事件。

IE8及以下版本,事件對象不作為參數傳遞,而是通過window對象的event屬性讀取,並且事件對象的target屬性叫做srcElement屬性。所以,以前獲取事件信息,往往要寫成下面這樣。

function myEventHandler(event) {
var actualEvent = event || window.event;
var actualTarget = actualEvent.target || actualEvent.srcElement;
// ...
}
上面的代碼只是為了說明以前的程序為什么這樣寫,在新代碼中,這樣的寫法不應該再用了。

以下介紹Event實例的屬性和方法。

4.1 bubbles,eventPhase
以下屬性與事件的階段有關

4.1.1 bubbles
bubbles屬性返回一個布爾值,表示當前事件是否會冒泡。該屬性為只讀屬性,只能在新建事件時改變。除非顯式聲明,Event構造函數生成的事件,默認是不冒泡的。

function goInput(e) {
if (!e.bubbles) {
passItOn(e);
} else {
doOutput(e);
}
}
上面代碼根據事件是否冒泡,調用不同的函數。

4.1.2 eventPhase
eventPhase屬性返回一個整數值,表示事件目前所處的節點

var phase = event.eventPhase;
0,事件目前沒有發生。
1,事件目前處於捕獲階段,即處於從祖先節點向目標節點的傳播過程中。該過程是從Window對象到Document節點,再到HTMLHtmlElement節點,直到目標節點的父節點為止。
2,事件到達目標節點,即target屬性指向的那個節點。
3,事件處於冒泡階段,即處於從目標節點向祖先節點的反向傳播過程中。該過程是從父節點一直到Window對象。只有bubbles屬性為true時,這個階段才可能發生。
4.2 cancelable,defaultPrevented
以下屬性與事件的默認行為有關。

4.2.1 cancelable
cancelable屬性返回一個布爾值,表示事件是否可以取消。該屬性為只讀屬性,只能在新建事件時改變。除非顯式聲明,Event構造函數生成的事件,默認是不可以取消的。

var bool = event.cancelable;
如果要取消某個事件,需要在這個事件上面調用preventDefault方法,這會阻止瀏覽器對某種事件部署的默認行為。

4.2.2 defaultPrevented
defaultPrevented屬性返回一個布爾值,表示該事件是否調用過preventDefault方法。會阻止瀏覽器對某種事件部署的默認行為。

if (e.defaultPrevented) {
// ...
}
4.3 currentTarget,target
以下屬性與事件的目標節點有關。

4.3.1 currentTarget
currentTarget屬性返回事件當前所在的節點,即正在執行的監聽函數所綁定的那個節點。作為比較,target屬性返回事件發生的節點。如果監聽函數在捕獲階段和冒泡階段觸發,那么這兩個屬性返回的值是不一樣的。

function hide(e){
console.log(this === e.currentTarget); // true
e.currentTarget.style.visibility = "hidden";
}

para.addEventListener('click', hide, false);
上面代碼中,點擊para節點,該節點會不可見。另外,在監聽函數中,currentTarget屬性實際上等同於this對象。

4.3.2 target
target屬性返回觸發事件的那個節點,即事件最初發生的節點。如果監聽函數不在該節點觸發,那么它與currentTarget屬性返回的值是不一樣的。

function hide(e){
console.log(this === e.target); // 有可能不是true
e.target.style.visibility = "hidden";
}

// HTML代碼為
// <p id="para">Hello <em>World</em></p>
para.addEventListener('click', hide, false);
上面代碼中,如果在para節點的em子節點上面點擊,則e.target指向em子節點,導致em子節點(即World部分)會不可見,且輸出false。

在IE6—IE8之中,該屬性的名字不是target,而是srcElement,因此經常可以看到下面這樣的代碼。

function hide(e) {
var target = e.target || e.srcElement;
target.style.visibility = 'hidden';
}
4.4 type,detail,timeStamp,isTrusted
以下屬性與事件對象的其他信息相關。

4.4.1 type
type屬性返回一個字符串,表示事件類型,具體的值同addEventListener方法和removeEventListener方法的第一個參數一致,大小寫不敏感。

var string = event.type;
4.4.2 detail
detail屬性返回一個數值,表示事件的某種信息。具體含義與事件類型有關,對於鼠標事件,表示鼠標按鍵在某個位置按下的次數,比如對於dblclick事件,detail屬性的值總是2。

function giveDetails(e) {
this.textContent = e.detail;
}

el.onclick = giveDetails;
4.4.3 timeStamp
timeStamp屬性返回一個毫秒時間戳,表示事件發生的時間。

var number = event.timeStamp;
4.4.4 isTrusted
isTrusted屬性返回一個布爾值,表示該事件是否可以信任。

var bool = event.isTrusted;
Firefox瀏覽器中,用戶觸發的事件會返回true,腳本觸發的事件返回false;IE瀏覽器中,除了使用createEvent方法生成的事件,所有其他事件都返回true;Chrome瀏覽器不支持該屬性。

4.5 preventDefault()
preventDefault方法取消瀏覽器對當前事件的默認行為,比如點擊鏈接后,瀏覽器跳轉到指定頁面,或者按一下空格鍵,頁面向下滾動一段距離。該方法生效的前提是,事件的cancelable屬性為true,如果為false,則調用該方法沒有任何效果。

該方法不會阻止事件的進一步傳播(stopPropagation方法可用於這個目的)。只要在事件的傳播過程中(捕獲階段、目標階段、冒泡階段皆可),使用了preventDefault方法,該事件的默認方法就不會執行。

// HTML代碼為
// <input type="checkbox" id="my-checkbox" />

var cb = document.getElementById('my-checkbox');

cb.addEventListener(
'click',
function (e){ e.preventDefault(); },
false
);
上面代碼為點擊單選框的事件,設置監聽函數,取消默認行為。由於瀏覽器的默認行為是選中單選框,所以這段代碼會導致無法選中單選框。

利用這個方法,可以為文本輸入框設置校驗條件。如果用戶的輸入不符合條件,就無法將字符輸入文本框。

function checkName(e) {
if (e.charCode < 97 || e.charCode > 122) {
e.preventDefault();
}
}
上面函數設為文本框的keypress監聽函數后,將只能輸入小寫字母,否則輸入事件的默認事件(寫入文本框)將被取消。

如果監聽函數最后返回布爾值false(即return false),瀏覽器也不會觸發默認行為,與preventDefault方法有等同效果。

4.6 stopPropagation()
stopPropagation方法阻止事件在DOM中繼續傳播,防止再觸發定義在別的節點上的監聽函數,但是不包括在當前節點上新定義的事件監聽函數。

function stopEvent(e) {
e.stopPropagation();
}

el.addEventListener('click', stopEvent, false);
將上面函數指定為監聽函數,會阻止事件進一步冒泡到el節點的父節點。

4.7 stopImmediatePropagation()
stopImmediatePropagation方法阻止同一個事件的其他監聽函數被調用。

如果同一個節點對於同一個事件指定了多個監聽函數,這些函數會根據添加的順序依次調用。只要其中有一個監聽函數調用了stopImmediatePropagation方法,其他的監聽函數就不會再執行了。

function l1(e){
e.stopImmediatePropagation();
}

function l2(e){
console.log('hello world');
}

el.addEventListener('click', l1, false);
el.addEventListener('click', l2, false);
上面代碼在el節點上,為click事件添加了兩個監聽函數l1和l2。由於l1調用了stopImmediatePropagation方法,所以l2不會被調用。

4.8 event.returnValue=false和return false
4.8.1 returnValue
returnValue是IE的一個屬性,如果設置了該屬性,它的值比事件句柄的返回值優先級要高,把它的值設置為false,可以取消發生事件源元素的默認動作;

returnValue:設置或獲取事件的返回值。

event.returnValue:當捕捉到事件(event)時會做一些判斷,判斷失敗,則會阻止事件繼續執行,可以達到的效果是“不能在輸入框中輸入非數字字符”,window.event.returnValue=false放在提交表單中的onclick事件中則不會提交表單,如果放到超鏈接中則不執行超鏈接。

4.8.2 return false
return false就是返回一個false值。禁止一些瀏覽器的默認行為,由於原先默認的行為是ture,例如,<a>鏈接,點擊事件發生后,緊接着的默認事件就是跳轉鏈接,但是,在οnclick=function(){return false;}之后,就可以對緊接着的默認行為禁止掉;

另外:如果想同時阻止事件冒泡(event.stopPropagation())和阻止瀏覽器行為(event.preventDefault());這個是同時調用阻止事件冒泡和阻止瀏覽器行為的簡要寫法

這兩個的區別是:基本用法一樣都是阻止默認行為,但是return false;后面的代碼不在執行,但是event.returnValue=false;后面的代碼還是可以繼續執行的
————————————————
版權聲明:本文為CSDN博主「空山新雨天氣晚秋」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/u012060033/article/details/89785554


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM