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

本文並不是一篇實用的文字,不考慮兼容性,而在於機制的理解。
關於本文的題目,不叫“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
inner,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]。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章