JavaScript 事件傳播

在本教程中,你將瞭解事件如何在 JavaScript 中的 DOM 樹中傳播。

瞭解事件傳播

事件傳播是一種機制,它定義事件如何通過 DOM 樹傳播或傳播到達目標,以及之後發生的事情。

讓我們在一個例子的幫助下理解這一點,假設你已經在一個巢狀在一個段落(即<p>元素)中的超連結(即 <a> 元素)上分配了一個 click 事件處理程式。現在,如果單擊該連結,將執行處理程式。但是,如果將 click 事件處理程式分配給包含該連結的段落,而不是連結,那麼即使在這種情況下,單擊該連結仍將觸發處理程式。這是因為事件不僅影響生成事件的目標元素 - 它們在 DOM 樹中上下移動以到達目標。這稱為事件傳播。

在現代瀏覽器中,事件傳播分兩個階段進行: 捕獲冒泡階段。在我們繼續前進之前,請看一下下圖:

事件傳播演示

上圖顯示了在具有父元素的元素上觸發事件時,事件在事件傳播的不同階段中事件如何在 DOM 樹中傳播。

引入事件傳播的概念是為了處理 DOM 層次結構中具有父子關係的多個元素具有針對同一事件的事件處理程式的情況,例如滑鼠單擊。現在,問題是當使用者點選內部元素 - 外部元素的 click 事件或內部元素時,將首先處理哪個元素的 click 事件。

在本章的以下部分中,我們將更詳細地討論事件傳播的每個階段,並找出該問題的答案。

注意: 正式的有 3 個階段,捕獲目標泡沫階段。但是,第二階段即目標階段(在事件到達已生成事件的目標元素時發生)在現代瀏覽器中不單獨處理,因此我們用捕獲冒泡兩個階段來處理程式。

捕獲階段

在捕獲階段,事件從 Window 向下傳播通過 DOM 樹到目標節點。例如,如果使用者單擊超連結,則該單擊事件將通過元素 <html><body> 元素和包含該連結的<p>元素。

此外,如果目標元素的任何祖先(即父,祖父等)和目標本身具有針對該型別事件的特別註冊的捕獲事件監聽器,則在該階段期間執行這些監聽器。我們來看看下面的例子:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Capturing Demo</title>
<style type="text/css">
    div, p, a{
        padding: 15px 30px;
        display: block;
        border: 2px solid #000;
        background: #fff;
    }
</style>
</head>
<body>
<div id="wrap">DIV
    <p class="hint">P
        <a href="#">A</a>
    </p>
</div>

<script>
    function showTagName() {
        alert("Capturing: "+ this.tagName);
    }
    
    var elems = document.querySelectorAll("div, p, a");
    for(let elem of elems) {
        elem.addEventListener("click", showTagName, true);
    }
</script>
</body>
</html>

以下是我們利用上面的示例建立的簡單演示,向你展示事件捕獲的工作原理。單擊任何元素並觀察出現警報彈出視窗的順序。

<div id="wrap">
<p class="hint">
<a href="#">Click Me</a>
</p>

</div>

並非所有瀏覽器都支援事件捕獲,而且它很少使用。例如,版本 9.0 之前的 Internet Explorer 不支援事件捕獲。

此外,事件捕獲僅適用於 addEventListener() 在第三個引數設定為 true 時使用該方法註冊的事件處理程式。分配事件處理程式,如使用的傳統方法 onclickonmouseover 等會不會在這裡工作。請檢視 JavaScript 事件監聽器 章節以瞭解有關事件監聽器的更多資訊。

冒泡階段

在冒泡階段,恰好相反。在這個階段,事件傳播或冒泡 DOM 樹,從目標元素到視窗,逐個訪問目標元素的所有祖先。例如,如果使用者單擊超連結,則該單擊事件將通過 <p> 包含連結, <body> 元素, <html> 元素和 document 節點的元素。

此外,如果目標元素的任何祖先和目標本身具有為該型別的事件分配的事件處理程式,則在此階段執行這些處理程式。在現代瀏覽器中,預設情況下,所有事件處理程式都在冒泡階段進行註冊。我們來看一個例子:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Bubbling Demo</title>
<style type="text/css">
    div, p, a{
        padding: 15px 30px;
        display: block;
        border: 2px solid #000;
        background: #fff;
    }
</style>
</head>
<body>
<div onclick="alert('Bubbling: ' + this.tagName)">DIV
    <p onclick="alert('Bubbling: ' + this.tagName)">P
        <a href="#" onclick="alert('Bubbling: ' + this.tagName)">A</a>
    </p>
</div>
</body>
</html>

這是一個簡單的演示,我們利用上面的例子建立了一個簡單的演示,向你展示事件冒泡是如何工作的。單擊任何元素並觀察出現警報彈出視窗的順序。

<div id="wrap">
<p class="hint">
<a href="#">Click Me</a>
</p>

</div>

所有瀏覽器都支援事件冒泡,它適用於所有處理程式,無論它們如何註冊,例如使用 onclickaddEventListener() (除非它們被註冊為捕獲事件監聽器)。這就是為什麼術語事件傳播經常被用作事件冒泡的同義詞。

訪問目標元素

目標元素是生成事件的 DOM 節點 。例如,如果使用者單擊超連結,則目標元素是超連結。

目標元素是可訪問的 event.target ,它不會通過事件傳播階段發生變化。此外, this 關鍵字表示當前元素(即附加有當前正在執行的處理程式的元素)。我們來看一個例子:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Target Demo</title>
<style type="text/css">
    div, p, a{
        padding: 15px 30px;			
        display: block;
        border: 2px solid #000;
        background: #fff;
    }
</style>
</head>
<body>
<div id="wrap">DIV
    <p class="hint">P
        <a href="#">A</a>
    </p>
</div>

<script>
    // Selecting the div element
    var div = document.getElementById("wrap");

    // Attaching an onclick event handler
    div.onclick = function(event) {
        event.target.style.backgroundColor = "lightblue";

        // Let the browser finish rendering of background color before showing alert
        setTimeout(() => {
            alert("target = " + event.target.tagName + ", this = " + this.tagName);
            event.target.style.backgroundColor = ''
        }, 0);
    }
</script>
</body> 
</html>

這是我們利用上面的例子建立的簡單演示。單擊任何元素,它將顯示目標元素的標記名稱和當前元素。

<div id="wrap">
<p class="hint">
<a href="#">Click Me</a>
</p>

</div>

我們在上面的例子中使用的胖箭頭(=>)符號是箭頭函式表示式。它的語法比函式表示式短,並且可以使 this 關鍵字正常執行。請檢視有關 ES6 功能 的教程,以瞭解有關箭頭功能的更多資訊。

停止事件傳播

如果要阻止任何祖先元素的事件處理程式收到有關該事件的通知,你還可以在中間停止事件傳播。

例如,假設你有巢狀元素,並且每個元素都有 onclick 顯示警告對話方塊的事件處理程式。通常,當你單擊內部元素時,將立即執行所有處理程式,因為事件會冒泡到 DOM 樹。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Propagation Demo</title>
<style type="text/css">
    div, p, a{
        padding: 15px 30px;
        display: block;
        border: 2px solid #000;
        background: #fff;
    }
</style>
</head>
<body>
<div id="wrap">DIV
    <p class="hint">P
        <a href="#">A</a>
    </p>
</div>

<script>
    function showAlert() {
        alert("You clicked: "+ this.tagName);
    }

    var elems = document.querySelectorAll("div, p, a");
    for(let elem of elems) {
        elem.addEventListener("click", showAlert);
    }
</script>
</body>
</html>

這是我們利用上面的例子建立的簡單演示。如果單擊任何子元素,則還會執行父元素上的事件處理程式,你可能會看到多個警告框。

<div id="wrap">
<p class="hint">
<a href="#">Click Me</a>
</p>

</div>

為了防止出現這種情況,你可以使用 event.stopPropagation() 方法阻止事件冒泡 DOM 樹。在以下示例中,如果單擊子元素,則不會執行父元素上的單擊事件偵聽器。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Stop Event Propagation Demo</title>
<style type="text/css">
    div, p, a{
        padding: 15px 30px;
        display: block;
        border: 2px solid #000;
        background: #fff;
    }
</style>
</head>
<body>
<div id="wrap">DIV
    <p class="hint">P
        <a href="#">A</a>
    </p>
</div>

<script>
    function showAlert(event) {
        alert("You clicked: "+ this.tagName);
        event.stopPropagation();
    }

    var elems = document.querySelectorAll("div, p, a");
    for(let elem of elems) {
        elem.addEventListener("click", showAlert);
    }
</script>
</body>
</html>

這是更新的演示。現在,如果單擊任何子元素,則只會出現一個警報。

<div id="wrap">
<p class="hint">
<a href="#">Click Me</a>
</p>

</div>

此外,你甚至可以使用 stopImmediatePropagation() 方法來阻值連線到同一元素的任何其他偵聽器以獲取相同的事件型別執行。

在下面的示例中,我們將多個偵聽器附加到超連結,但是當你單擊該連結時,只會執行一個超連結偵聽器,並且你將只看到一個警報。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Stop Immediate Propagation Demo</title>
<style type="text/css">
    div, p, a{
        padding: 15px 30px;
        display: block;
        border: 2px solid #000;
        background: #fff;
    }
</style>
</head>
<body>
<div onclick="alert('You clicked: ' + this.tagName)">DIV
    <p onclick="alert('You clicked: ' + this.tagName)">P
        <a href="#" id="link">A</a>
    </p>
</div>

<script>
    function sayHi() {
        alert("Hi, there!");
        event.stopImmediatePropagation();
    }
    function sayHello() {
        alert("Hello World!");
    }
    
    // Attaching multiple event handlers to hyperlink
    var link = document.getElementById("link");
    link.addEventListener("click", sayHi);  
    link.addEventListener("click", sayHello);
</script>
</body>
</html>

注意: 如果多個偵聽器附加到同一事件型別的同一元素,則它們將按照新增它們的順序執行。但是,如果任何偵聽器呼叫該 event.stopImmediatePropagation() 方法,則不會執行任何剩餘的偵聽器。

防止預設操作

某些事件具有與之關聯的預設操作。例如,如果單擊連結瀏覽器,則會轉到連結的目標,當你單擊表單提交按鈕時,瀏覽器提交表單等。你可以使用事件物件的 preventDefault() 方法阻止此類預設操作。

但是,阻止預設操作不會停止事件傳播; 事件繼續像往常一樣傳播到 DOM 樹。這是一個例子:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Prevent Default Demo</title>
</head>
<body>
<form action="/examples/html/action.php" method="post" id="users">
    <label>First Name:</label>
    <input type="text" name="first-name" id="firstName">
    <input type="submit" value="Submit" id="submitBtn">
</form>

<script>
    var btn = document.getElementById("submitBtn");
    
    btn.addEventListener("click", function(event) {
        var name = document.getElementById("firstName").value;
        alert("Sorry, " + name + ". The preventDefault() won't let you submit this form!");
        event.preventDefault(); // Prevent form submission
    });
</script>
</body>
</html>