瀏覽器事件模型中捕獲階段、目標階段、冒泡階段實例詳解

若是對事件大概瞭解,可能知道有事件冒泡這回事,可是冒泡、捕獲、傳播這些機制可能尚未深刻的研究實踐一下,我抽時間整理了一下相關的知識。javascript

  • 本文主要對事件機制一些細節進行討論,過於基礎的事件綁定知識方法沒有介紹。
  • 特別少的篇幅關注瀏覽器兼容問題,畢竟原理了解了,兼容性問題能夠本身想辦法解決了。

在瀏覽器相對標準化以前,各個瀏覽器廠商都是本身實現的事件模型,有的用了冒泡,有的用了捕獲,W3C爲了兼顧以前的標準,將事件發生定義成以下三個階段:css

一、捕獲階段
二、目標階段
三、冒泡階段

只是硬生生的說事件機制究竟是怎麼回事不容易理解,用一個demo爲主線說明事件的原理比較容易理解:html

HTML前端

<body>
    <div id="wrapDiv">wrapDiv
        <p id="innerP">innerP
            <span id="textSpan">textSpan</span>
        </p>
    </div>
</body>

CSSjava

<style>
    #wrapDiv, #innerP, #textSpan{
        margin: 5px;
        padding: 5px;
        box-sizing: border-box;
        cursor: default;
    }
    #wrapDiv{
        width: 300px;
        height: 300px;
        border: indianred 3px solid;
    }
    #innerP{
        width: 200px;
        height: 200px;
        border: hotpink 3px solid;
    }
    #textSpan{
        display: block;
        width: 100px;
        height: 100px;
        border: orange 3px solid;
    }
</style>

JavaScriptnode

<script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 捕獲階段綁定事件
    window.addEventListener("click", function(e){
        console.log("window 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.addEventListener("click", function(e){
        console.log("document 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.body.addEventListener("click", function(e){
        console.log("body 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    // 冒泡階段綁定的事件
    window.addEventListener("click", function(e){
        console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.addEventListener("click", function(e){
        console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.body.addEventListener("click", function(e){
        console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);
</script>

demo頁面效果圖
圖片描述瀏覽器

這個時候,若是點擊一下textSpan這個元素,控制檯會打印出這樣的內容:
圖片描述微信

當按下鼠標點擊後,到底發生了什麼的,如今我基於上面的例子來講一下:工具

capture=>start: 捕獲階段開始
window=>operation: window
document=>operation: document
documentElement=>operation: documentElement
body=>operation: body
wrapDiv=>operation: wrapDiv
innerP=>operation: innerP
target=>start: 捕獲階段結束,目標階段開始
textSpan=>operation: textSpan
textSpan2=>operation: textSpan
bubble=>start: 目標階段結束,冒泡階段開始
innerP2=>operation: innerP
wrapDiv2=>operation: wrapDiv
body2=>operation: body
documentElement2=>operation: documentElement
document2=>operation: document
window2=>operation: window
bubbleend=>start: 冒泡階段結束
capture->window->document->documentElement->body->wrapDiv->innerP->target->textSpan->textSpan2->bubble->innerP2->wrapDiv2->body2->documentElement2->document2->window2->bubbleend

從上面所畫的事件傳播的過程可以看出來,當點擊鼠標後,會先發生事件的捕獲測試

  • 捕獲階段:首先window會獲捕獲到事件,以後documentdocumentElementbody會捕獲到,再以後就是在body中DOM元素一層一層的捕獲到事件,有wrapDivinnerP
  • 目標階段:真正點擊的元素textSpan的事件發生了兩次,由於在上面的JavaScript代碼中,textSapn既在捕獲階段綁定了事件,又在冒泡階段綁定了事件,因此發生了兩次。可是這裏有一點是須要注意,在目標階段並不必定先發生在捕獲階段所綁定的事件,而是先綁定的事件發生,一會會解釋一下。
  • 冒泡階段:會和捕獲階段相反的步驟將事件一步一步的冒泡到window

那可能有一個疑問,咱們不用addEventListener綁定的事件會發生在哪一個階段呢,咱們來一個測試,順便再演示一下我在上面的目標階段所說的目標階段並不必定先發生捕獲階段所綁定的事件是怎麼一回事。
咱們從新改一下JavaScript代碼:

<script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 測試直接綁定的事件到底發生在哪一個階段
    wrapDiv.onclick = function(){
        console.log("wrapDiv onclick 測試直接綁定的事件到底發生在哪一個階段")
    };

    // 捕獲階段綁定事件
    window.addEventListener("click", function(e){
        console.log("window 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.addEventListener("click", function(e){
        console.log("document 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.body.addEventListener("click", function(e){
        console.log("body 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    textSpan.addEventListener("click", function(){
        console.log("textSpan 冒泡 在捕獲以前綁定的")
    }, false);

    textSpan.onclick = function(){
        console.log("textSpan onclick")
    };

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    // 冒泡階段綁定的事件
    window.addEventListener("click", function(e){
        console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.addEventListener("click", function(e){
        console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.body.addEventListener("click", function(e){
        console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);
</script>

再看控制檯的結果:
圖片描述

  • 圖中第一個被圈出來的解釋:textSpan是被點擊的元素,也就是目標元素,全部在textSpan上綁定的事件都會發生在目標階段,在綁定捕獲代碼以前寫了綁定的冒泡階段的代碼,因此在目標元素上就不會遵照先發生捕獲後發生冒泡這一規則,而是先綁定的事件先發生。
  • 圖中第二個被圈出來的解釋:因爲wrapDiv不是目標元素,因此它上面綁定的事件會遵照先發生捕獲後發生冒泡的規則。因此很明顯用onclick直接綁定的事件發生在了冒泡階段。

target和currentTarget

上面的代碼中寫了e.targete.currentTarget,尚未說是什麼,targetcurrentTarget都是event上面的屬性,target是真正發生事件的DOM元素,而currentTarget是當前事件發生在哪一個DOM元素上。
能夠結合控制檯打印出來的信息理解下,目標階段也就是 target == currentTarget的時候。我沒有打印它們兩個由於太長了,因此打印了它們的nodeName,可是因爲window沒有nodeName這個屬性,因此是undefined

阻止事件傳播

說到事件,必定要說的是如何阻止事件傳播。老是有不少帖子說e.stopPropagation()是阻止事件的冒泡的傳播,實際上這麼說並非很準確,由於它不只能夠阻止事件在冒泡階段的傳播,還能阻止事件在捕獲階段的傳播。
來看一下咱們再改一下的JavaScript代碼:

<script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 測試直接綁定的事件到底發生在哪一個階段
    wrapDiv.onclick = function(){
        console.log("wrapDiv onclick 測試直接綁定的事件到底發生在哪一個階段")
    };

    // 捕獲階段綁定事件
    window.addEventListener("click", function(e){
        console.log("window 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.addEventListener("click", function(e){
        console.log("document 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.body.addEventListener("click", function(e){
        console.log("body 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 捕獲", e.target.nodeName, e.currentTarget.nodeName);
        // 在捕獲階段阻止事件的傳播
        e.stopPropagation();
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    textSpan.addEventListener("click", function(){
        console.log("textSpan 冒泡 在捕獲以前綁定的")
    }, false);

    textSpan.onclick = function(){
        console.log("textSpan onclick")
    };

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    // 冒泡階段綁定的事件
    window.addEventListener("click", function(e){
        console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.addEventListener("click", function(e){
        console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.body.addEventListener("click", function(e){
        console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);
</script>

咱們在事件的捕獲階段阻止了傳播,看一下控制檯的結果:
圖片描述
實際上咱們點擊的是textSpan,可是因爲在捕獲階段事件就被阻止了傳播,因此在textSpan上綁定的事件根本就沒有發生,冒泡階段綁定的事件天然也不會發生,由於阻止事件在捕獲階段傳播的特性,e.stopPropagation()不多用到在捕獲階段去阻止事件的傳播,你們就覺得e.stopPropagation()只能阻止事件在冒泡階段傳播。

阻止事件的默認行爲

e.preventDefault()能夠阻止事件的默認行爲發生,默認行爲是指:點擊a標籤就轉跳到其餘頁面、拖拽一個圖片到瀏覽器會自動打開、點擊表單的提交按鈕會提交表單等等,由於有的時候咱們並不但願發生這些事情,因此須要阻止默認行爲,這塊的知識比較簡單,能夠本身去試一下。

與事件相關的兼容性問題

這裏只是簡單提一下兼容性問題,不作過多的展開。對於綁定事件,ie低版本的瀏覽器是用attachEvent,而高版本ie和標準瀏覽器用的是addEventListenerattachEvent不能指定綁定事件發生在捕獲階段仍是冒泡階段,它只能將事件綁定到冒泡階段,可是並不意味這低版本的ie沒有事件捕獲,它也是先發生事件捕獲,再發生事件冒泡,只不過這個過程沒法經過程序控制。

其實事件的兼容性問題特別的多,好比獲取事件對象的方式、綁定和解除綁定事件的方式、目標元素的獲取方式等等,因爲古老的瀏覽器終究會被淘汰,不過多展開了。

歡迎關注【本期節目】,微信公衆號ID:benqijiemu。
這裏有:互聯網思考、軟件&工具推薦、前端技術等。
能夠在公衆號回覆我,但願和你們一塊兒交流全部與互聯網相關的事情~
圖片描述

相關文章
相關標籤/搜索