關於瀏覽器裡事件的捕獲和冒泡及監聽器執行的順序

stu_wei發表於2018-08-27

本文並不是一篇實用的文字,不考慮相容性,而在於機制的理解。
關於本文的題目,不叫“js事件的捕獲和冒泡”,是因為碼者並不清楚這種叫法準不準確,於是用一個不那麼精確的“瀏覽器”一詞。
測試環境:Firefox Quantum 61.0.2 (64 位)

發現問題(場景)

下面n段程式碼的輸出?

(程式碼一)基本的巢狀

<div onclick="outer()">
	<div onclick="middle()">
		<div onclick="inner()">gogo</div>
	</div>
</div>

function outer(){
	console.log('outer');
}
function middle(){
	console.log('middle');
}
function inner(){
	console.log('inner');
}

結果

inner
middle
outer

思考:看起來,內層dom的事件監聽函式先執行(不過,過早的下結論是很不明智的)。也就是當父子標籤都有事件註冊的時候,點選子元件 => 父子標籤的監聽器都會執行(當然,點選父元件的其他區域,子元件的監聽器不會執行)。但是,像css一樣,子標籤自己的東西(比如font-size),優先順序高一點。

(程式碼二)換一種事件註冊方式: addEventListener()

<div id="outer">
	<div id="middle">
		<div id="inner">gogo</div>
	</div>
</div>

// 獲取dom
function get(id){
	return document.getElementById(id);
}
get('outer').addEventListener('click',function(e){
	console.log('outer');
});
get('middle').addEventListener('click',function(e){
	console.log('middle');
});
get('inner').addEventListener('click',function(e){
	console.log('inner');
});

結果

inner
middle
outer

思考: 這裡看起來沒什麼區別,但是,其實addEventListener()的引數不止兩個。

(程式碼三)addEventListener() 的第三個引數

第三個引數的預設值是false,這裡我們先觀察一下值為true的情況。

<div id="outer">
	<div id="middle">
		<div id="inner">gogo</div>
	</div>
</div>

function get(id){
	return document.getElementById(id);
}
get('outer').addEventListener('click',function(e){
	console.log('outer');
},true);
get('middle').addEventListener('click',function(e){
	console.log('middle');
},true);
get('inner').addEventListener('click',function(e){
	console.log('inner');
},true);

結果

outer
middle
inner

思考: 執行順序從由內到外,變成從外到內了。先不要去考慮其原理。從應用的角度來說,如果業務邏輯需要先執行外層監聽器,後執行內層監聽器,那麼,addEventListener()很合適。

(程式碼四)混合一下

addEventListener() 第三個引數既有false又有true會怎樣?

<div id="outer">
	<div id="middle">
		<div id="inner">gogo</div>
	</div>
</div>

// 三個引數
get('outer').addEventListener('click',function(e){
	console.log('outer');
},true);
get('middle').addEventListener('click',function(e){
	console.log('middle');
},true);
get('inner').addEventListener('click',function(e){
	console.log('inner');
},true);
// 兩個引數(或者第三個引數為false)
get('outer').addEventListener('click',function(e){
	console.log('outer,false');
});
get('middle').addEventListener('click',function(e){
	console.log('middle,false');
});
get('inner').addEventListener('click',function(e){
	console.log('inner,false');
});

結果

outer
middle
inner
inner,false
middle,false
outer,false

思考: 這裡有點亂,面臨“亂”,可以從原理的角度思考這個問題,先把這個亂放在一邊,之後再回來看看這段程式碼。

事件的捕獲和冒泡

所以,這裡才是正文的開始[偷笑.jpg]

<div onclick="outer()">
	<div onclick="middle()">
		<div onclick="inner()">gogo</div>
	</div>
</div>
從滑鼠點選“gogo”,到控制檯列印出“outer”,這段時間發生了什麼?

捕獲與冒泡
第一階段: 事件捕獲
每個div都像一個紙盒子(俄羅斯套娃瞭解一下),外層div盒子裡,有內層div盒子(月餅盒瞭解一下?)。那麼如果你用手點這個紙盒子,肯定是外層的先接收到訊號,外層紙盒子被點出一個凹槽(這個盒子比較軟),這個凹槽的底部會碰到內層盒子,於是內層紙盒子接受到訊號。
事件的捕獲是由外而內的。
第二階段: 事件冒泡
冒泡這個詞本身就解釋了這個過程的順序,肯定是從裡往外冒啊。

也就是,當滑鼠點選到“gogo”後:

  1. 外層div先捕獲到這個點選事件
  2. 然後內層div捕獲到這個點選事件
  3. 內層div(的監聽器)處理這次事件
  4. 外層div(的監聽器)處理這個點選事件

新的問題

按上面的說法,事件處理的監聽器應該是由內而外執行啊,但是上面的程式碼(當addEventListener第三個引數為true時)並不符合這個規則。有一個錯誤的結論是,當addEventListener第三個引數為true時,監聽器會在捕獲階段就執行,false時,在冒泡階段執行,其實根據這個結論是完全解釋得通上面所有的程式碼的,特別是上面最後一段。這也是很多人正在犯的錯誤,下面這段程式碼證明了這個結論的錯誤

(程式碼五)

<div id="outer">
	<div id="inner">
		gogo
	</div>
</div>

get('inner').addEventListener('click',function(e){
	console.log('inner,false');
},false);
get('inner').addEventListener('click',function(e){
	console.log('inner,true');
},true);

get('outer').addEventListener('click',function(e){
	console.log('outer,false');
},false);
get('outer').addEventListener('click',function(e){
	console.log('outer,true');
},true);

錯誤結論的結果:

outer,true
inner,true
inner,false
outer,false

實際的結果

outer,true
inner,false
inner,true
outer,false

當我第一次看到這個結果我可是一臉矇蔽。於是,我去mdn看了一下第三個引數,有下述文字:

(第三個引數是)A Boolean indicating whether events of this type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree.

我對著這句話看了好幾分鐘,又對照中文版也看了好幾分鐘,其意沒現還不是因為沒讀百遍?於是我又讀了好幾分鐘,有如下心得:

  • 首先第三個引數是個boolean
  • 然後一個boolean能代表什麼,當然是“是否”嘍
  • 於是看到了whether,那麼whether what? 我開始以為是this type will be的be,但是實際上是後面的before
  • 於是得出了這個whether的正反面
  • 正面:events (of this type) will be dispatched to the registered listener before being dispatched to any EventTarget (beneath it in the DOM tree). (這種事件會被dispatch到註冊了的監聽器上(dispatch了就會馬上執行),before 被dispatch到內層dom結點(就是那個 beneath it in the dom tree)的其他eventTarget上)
  • 反面:events (of this type) will be dispatched to the registered listener after(或者說not before) being dispatched to any EventTarget (beneath it in the DOM tree).

誰先誰後不如排個序,會看起來更明瞭一點(本文最重要的結論,如果上面的我沒解釋明白……記住下面這兩行應該是有好處的):

  • 事件被dispatch到監聽器上(馬上會執行)
  • 然後,事件被dispatch到內層dom結點的eventTarget上(只是到了target上,並沒交給listener,也就是不會馬上被執行)

也就是,在往內層傳遞點選事件之前,監聽器被執行,也就是先執行外層div的監聽器,內層才會接收到點選事件。

回到起點

前兩段程式碼

普通的事件捕獲和普通的事件冒泡

第三段程式碼

當滑鼠點到“gogo”時,outer先接收到了“點選”,因為它被註冊了一個監聽器(通過addEventListener),而且第三個引數是true,所以應該先執行自己的監聽器,再往middle傳事件。(也就是在middle沒捕獲“點選”之前,outer的監聽器就已經被執行了)。於是……,沒問題。

第四段程式碼

和第三段程式碼差不多,於是也沒什麼問題。

第五段程式碼

這是本文最後一個問題。因為addEventListener()的第三個引數是決定先往內層結點傳還是先自己處理監聽器,所以當沒有下級結點,這個引數還有什麼意義?,這時候,誰先註冊事件,誰就先執行(這是本文第二重要的結論)[嘿嘿,想不到吧.jpg]。

相關文章