關於瀏覽器裡事件的捕獲和冒泡及監聽器執行的順序
本文並不是一篇實用的文字,不考慮相容性,而在於機制的理解。
關於本文的題目,不叫“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”後:
- 外層div先捕獲到這個點選事件
- 然後內層div捕獲到這個點選事件
- 內層div(的監聽器)處理這次事件
- 外層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]。
相關文章
- JS中的事件順序(事件捕獲與冒泡)JS事件
- 關於js事件冒泡和事件捕獲JS事件
- 監聽瀏覽器的後退事件瀏覽器事件
- 理解js的事件冒泡和事件捕獲JS事件
- 瀏覽器執行緒執行順序,瞭解一下瀏覽器執行緒
- 事件的捕獲、冒泡、委託事件
- JavaScript事件捕獲冒泡與捕獲JavaScript事件
- Javascript中的事件冒泡與捕獲JavaScript事件
- 事件和事件監聽器事件
- 關於 Promise 的執行順序Promise
- 監聽瀏覽器返回,pushState,popstate 事件,window.history物件瀏覽器事件物件
- 關於describe和test執行順序的翻譯
- JavaScript 執行機制-瀏覽器事件迴圈JavaScript瀏覽器事件
- 關於UC瀏覽器相容scroll事件問題瀏覽器事件
- 聊聊如何讓springboot攔截器的執行順序按我們想要的順序執行Spring Boot
- 瀏覽器執行緒瀏覽器執行緒
- 瀏覽器執行原理瀏覽器
- 重學瀏覽器(1)-多程式多執行緒的瀏覽器瀏覽器執行緒
- 關於瀏覽器相容瀏覽器
- 監聽器,過濾器,攔截器的執行過程和對比過濾器
- 阻止捕獲和冒泡,阻止預設行為
- Gradio-Lite: 完全在瀏覽器裡執行的無伺服器 Gradio瀏覽器伺服器
- 瀏覽器多執行緒和js單執行緒瀏覽器執行緒JS
- 使用瀏覽器事件瀏覽器事件
- 瀏覽器事件解析瀏覽器事件
- 瀏覽器滑鼠事件瀏覽器事件
- 如何在瀏覽器裡執行 SAPGUI 的事務試讀版瀏覽器GUI
- 瀏覽器中的事件迴圈瀏覽器事件
- 瀏覽器的事件環機制瀏覽器事件
- 如何移除事件監聽器事件
- Spirit帶你徹底瞭解事件捕獲和冒泡機制事件
- 從程式和執行緒瞭解瀏覽器的工作原理執行緒瀏覽器
- 瀏覽器和Node不同的事件迴圈(Event Loop)瀏覽器事件OOP
- JS在瀏覽器中的執行機制JS瀏覽器
- 【總結】瀏覽器的執行緒與程式瀏覽器執行緒
- 瀏覽器事件系統瀏覽器事件
- JavaScript瀏覽器事件物件JavaScript瀏覽器事件物件
- 關於瀏覽器外掛的初步認識瀏覽器