事件冒泡
我們已經瞭解到網頁是由元素組成的 - 標題、文字段落、影像、按鈕等等 - 並且你可以監聽發生在這些元素上的事件。例如,你可以向一個按鈕新增監聽器,它會在使用者點選按鈕時執行。
我們還了解到這些元素可以彼此巢狀:例如,一個 <button> 可以放在一個 <div> 元素內部。在這種情況下,我們將 <div> 元素稱為父元素,將 <button> 稱為子元素。
在本節中,我們將瞭解當你向父元素新增事件監聽器,而使用者點選子元素時會發生什麼。
介紹事件冒泡
在父元素上設定監聽器
考慮這樣一個網頁
<div id="container">
<button>Click me!</button>
</div>
<pre id="output"></pre>
這裡,按鈕位於另一個元素內部,一個 <div> 元素。我們說這裡的 <div> 元素是它包含的元素的父元素。如果我們向父元素新增一個點選事件處理程式,然後點選按鈕,會發生什麼?
const output = document.querySelector("#output");
function handleClick(e) {
output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}
const container = document.querySelector("#container");
container.addEventListener("click", handleClick);
你會看到,當用戶點選按鈕時,父元素會觸發一個點選事件。
You clicked on a DIV element
這是有道理的:按鈕位於 <div> 內部,所以當你點選按鈕時,你實際上也隱式地點選了它所在的元素。
冒泡示例
如果我們向按鈕和父元素新增事件監聽器會發生什麼?
<body>
<div id="container">
<button>Click me!</button>
</div>
<pre id="output"></pre>
</body>
讓我們嘗試向按鈕、它的父元素(<div>)以及包含它們兩個的 <body> 元素新增點選事件處理程式。
const output = document.querySelector("#output");
function handleClick(e) {
output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}
const container = document.querySelector("#container");
const button = document.querySelector("button");
document.body.addEventListener("click", handleClick);
container.addEventListener("click", handleClick);
button.addEventListener("click", handleClick);
你會看到,當用戶點選按鈕時,所有三個元素都會觸發點選事件。
You clicked on a BUTTON element You clicked on a DIV element You clicked on a BODY element
在這種情況下
- 首先觸發按鈕上的點選事件
- 然後觸發它的父元素(
<div>元素)上的點選事件 - 最後觸發
<div>元素的父元素(<body>元素)上的點選事件。
我們稱這種行為為事件從最內層被點選的元素向上冒泡。
這種行為既有用,也可能導致意想不到的問題。在接下來的部分中,我們將看到它會導致什麼問題,並找到解決方案。
影片播放器示例
在這個示例中,我們的頁面包含一個影片,最初是隱藏的,還有一個標有“顯示影片”的按鈕。我們希望實現以下互動
- 當用戶點選“顯示影片”按鈕時,顯示包含影片的框,但不要立即開始播放影片。
- 當用戶點選影片時,開始播放影片。
- 當用戶點選框內影片以外的任何地方時,隱藏該框。
HTML 程式碼如下
<button>Display video</button>
<div class="hidden">
<video>
<source
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm"
type="video/webm" />
<p>
Your browser doesn't support HTML video. Here is a
<a href="rabbit320.mp4">link to the video</a> instead.
</p>
</video>
</div>
它包含
- 一個
<button>元素 - 一個
<div>元素,它最初具有class="hidden"屬性 - 一個巢狀在
<div>元素內部的<video>元素。
我們使用 CSS 來隱藏設定了 "hidden" 類的元素。
JavaScript 程式碼如下
const btn = document.querySelector("button");
const box = document.querySelector("div");
const video = document.querySelector("video");
btn.addEventListener("click", () => box.classList.remove("hidden"));
video.addEventListener("click", () => video.play());
box.addEventListener("click", () => box.classList.add("hidden"));
這添加了三個 'click' 事件監聽器
- 一個在
<button>上,它顯示包含<video>的<div> - 一個在
<video>上,它開始播放影片 - 一個在
<div>上,它隱藏影片
讓我們看看它是如何工作的
你應該看到,當你點選按鈕時,包含影片的框和影片都會顯示。但是當你點選影片時,影片開始播放,但框又隱藏了!
影片位於 <div> 內部 - 它屬於 <div> - 所以點選影片會執行兩個事件處理程式,導致這種行為。
使用 stopPropagation() 修復問題
正如我們在上一節中看到的,事件冒泡有時會造成問題,但有一種方法可以阻止它。Event 物件在它上面有一個名為 stopPropagation() 的函式,當在事件處理程式中呼叫時,它會阻止事件向上冒泡到任何其他元素。
我們可以透過將 JavaScript 程式碼更改為以下程式碼來修復我們當前的問題
const btn = document.querySelector("button");
const box = document.querySelector("div");
const video = document.querySelector("video");
btn.addEventListener("click", () => box.classList.remove("hidden"));
video.addEventListener("click", (event) => {
event.stopPropagation();
video.play();
});
box.addEventListener("click", () => box.classList.add("hidden"));
我們所做的只是在 <video> 元素的 'click' 事件的處理程式中,在事件物件上呼叫 stopPropagation()。這將阻止該事件向上冒泡到框。現在嘗試點選按鈕,然後點選影片
事件捕獲
事件傳播的另一種形式是事件捕獲。這類似於事件冒泡,但順序相反:因此,事件不是首先觸發在最內層的目標元素上,然後觸發在依次巢狀程度更低的元素上,而是首先觸發在巢狀程度最低的元素上,然後觸發在依次巢狀程度更高的元素上,直到到達目標元素為止。
事件捕獲預設情況下是停用的。要啟用它,你必須在 addEventListener() 中傳遞 capture 選項。
這個示例與我們之前看到的 冒泡示例 相同,只是我們使用了 capture 選項
<body>
<div id="container">
<button>Click me!</button>
</div>
<pre id="output"></pre>
</body>
const output = document.querySelector("#output");
function handleClick(e) {
output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}
const container = document.querySelector("#container");
const button = document.querySelector("button");
document.body.addEventListener("click", handleClick, { capture: true });
container.addEventListener("click", handleClick, { capture: true });
button.addEventListener("click", handleClick);
在這種情況下,訊息的順序是反過來的:<body> 事件處理程式首先觸發,然後是 <div> 事件處理程式,最後是 <button> 事件處理程式。
You clicked on a BODY element You clicked on a DIV element You clicked on a BUTTON element
為什麼要同時使用捕獲和冒泡?在過去,當瀏覽器比現在跨相容性差得多時,Netscape 只使用事件捕獲,而 Internet Explorer 只使用事件冒泡。當 W3C 決定嘗試標準化行為並達成共識時,他們最終得到了這個包含兩者,並且是現代瀏覽器實現的系統。
預設情況下,幾乎所有事件處理程式都在冒泡階段註冊,這在大多數情況下更有意義。
事件委託
在上一節中,我們研究了由事件冒泡引起的一個問題以及如何解決它。然而,事件冒泡不僅僅是令人討厭:它非常有用。特別是,它使事件委託成為可能。在這種實踐中,當我們希望在使用者與大量子元素中的任何一個進行互動時執行一些程式碼時,我們會在它們的父元素上設定事件監聽器,並讓發生在它們身上的事件向上冒泡到它們的父元素,而不是必須在每個子元素上單獨設定事件監聽器。
讓我們回到我們的第一個示例,我們將在使用者點選一個按鈕時設定整個頁面的背景顏色。假設頁面被分成 16 個瓦片,我們希望在使用者點選瓦片時將其設定為隨機顏色。
HTML 程式碼如下
<div id="container">
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
</div>
我們有一點 CSS,用於設定瓦片的大小和位置
.tile {
height: 100px;
width: 25%;
float: left;
}
現在在 JavaScript 中,我們可以為每個瓦片新增一個點選事件處理程式。但是,一個更簡單、更高效的選擇是在父元素上設定點選事件處理程式,並依賴事件冒泡來確保在使用者點選瓦片時處理程式被執行
function random(number) {
return Math.floor(Math.random() * number);
}
function bgChange() {
const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
return rndCol;
}
const container = document.querySelector("#container");
container.addEventListener("click", (event) => {
event.target.style.backgroundColor = bgChange();
});
輸出如下(嘗試點選周圍):
注意:在這個示例中,我們使用 event.target 來獲取事件的目標元素(即最內層的元素)。如果我們想訪問處理該事件的元素(在本例中是容器),我們可以使用 event.currentTarget。
注意:有關完整的原始碼,請檢視 useful-eventtarget.html;您也可以 在這裡即時執行 它。
target 和 currentTarget
如果你仔細檢視我們在本頁面中介紹的示例,你會發現我們使用事件物件的兩個不同屬性來訪問被點選的元素。在 在父元素上設定監聽器 中,我們使用 event.currentTarget。然而,在 事件委託 中,我們使用 event.target.
區別在於 target 指的是最初觸發事件的元素,而 currentTarget 指的是附加了該事件處理程式的元素。
當事件向上冒泡時,target 保持不變,而 currentTarget 對於附加在層次結構中不同元素上的事件處理程式來說是不同的。
如果我們稍微修改上面 冒泡示例,就可以看到這一點。我們使用與之前相同的 HTML 程式碼
<body>
<div id="container">
<button>Click me!</button>
</div>
<pre id="output"></pre>
</body>
JavaScript 程式碼幾乎相同,只是我們記錄了 target 和 currentTarget
const output = document.querySelector("#output");
function handleClick(e) {
const logTarget = `Target: ${e.target.tagName}`;
const logCurrentTarget = `Current target: ${e.currentTarget.tagName}`;
output.textContent += `${logTarget}, ${logCurrentTarget}\n`;
}
const container = document.querySelector("#container");
const button = document.querySelector("button");
document.body.addEventListener("click", handleClick);
container.addEventListener("click", handleClick);
button.addEventListener("click", handleClick);
請注意,當我們點選按鈕時,無論事件處理程式是附加到按鈕本身、<div> 還是 <body>,target 始終是按鈕元素。但是,currentTarget 標識了我們當前正在執行其事件處理程式的元素。
target 屬性通常在事件委託中使用,就像我們上面 事件委託 示例中一樣。
測試您的技能!
你已經讀到這篇文章的末尾,但你能記住最重要的資訊嗎?在你繼續學習之前,請驗證你是否已記住這些資訊 - 請檢視 測試你的技能:事件.
結論
現在,你應該已經瞭解了在這個早期階段你需要知道的關於 Web 事件的所有知識。正如所述,事件實際上並不屬於核心 JavaScript - 它們是在瀏覽器 Web API 中定義的。
此外,重要的是要理解 JavaScript 使用的不同上下文具有不同的事件模型 - 從 Web API 到其他領域,例如瀏覽器 WebExtensions 和 Node.js(伺服器端 JavaScript)。我們不希望你現在瞭解所有這些領域,但瞭解事件的基礎知識將有助於你在學習 Web 開發的道路上不斷前進。
注意:如果你遇到困難,可以在我們的 溝通渠道 中聯絡我們。
另請參閱
- domevents.dev - 一個非常有用的互動式遊樂場應用程式,它允許你透過探索來了解 DOM 事件系統的行為。
- 事件參考
- 事件順序(有關捕獲和冒泡的討論) - 彼得·保羅·科赫寫的一篇非常詳細的文章。