[前端 · 面試 ]JavaScript 之你不一定會的基礎題(二)

程式設計三昧發表於2021-08-13

最近我在做前端面試題總結系列,感興趣的朋友可以新增關注,歡迎指正、交流。

爭取每個知識點能夠多總結一些,至少要做到在面試時,針對每個知識點都可以侃起來,不至於啞火。

JavaScript 之你不一定會的基礎題

前言

在上一篇文章【前端 · 面試 】JavaScript 之你不一定會的基礎題(一)中,有同學產生了這樣一個疑惑:為什麼 click 事件的監聽函式中,this.idevent.target.id 的輸出值是不一樣的?

今天我們就來扒一扒這其中的原理。

題目

有如下的 HTML 文件結構:

<div id="parent">
    <div id="child" class="child">
        點我
    </div>
</div>

第一次執行如下 JavaScript 程式碼:

document.getElementById("parent").addEventListener("click", function () {
    alert(`parent 事件觸發,` + this.id);
});

document.getElementById("child").addEventListener("click", function () {
    alert(`child 事件觸發,` + this.id);
});

第二次執行另一套 JavaScript 程式碼:

document.getElementById("parent").addEventListener("click", function (e) {
    alert(`parent 事件觸發,` + e.target.id);
});

document.getElementById("child").addEventListener("click", function (e) {
    alert(`child 事件觸發,` + e.target.id);
});

問題如下:

點選 id 為 child 的 div 後,JavaScript 程式碼的執行結果分別是什麼?

答案是:

  • 第一次結果為:先彈出“child 事件觸發,child”,再彈出“parent 事件觸發,parent”。
  • 第二次結果為:先彈出“child 事件觸發,child”,再彈出“parent 事件觸發,child”。

對於這個答案中的第二次輸出結果,有人生出了疑惑:為什麼 parent 事件觸發時,e.target.id 的結果為 child呢?不應該是 parent 嗎?

解惑

DOM 元素事件執行順序

首先,我們知道,HTML 頁面上 DOM 元素的事件執行順序一般有三個階段:

  • 事件捕獲
  • 事件觸發
  • 事件冒泡

整個過程如下圖:

image-20210813192245058

事件捕獲和事件冒泡

當一個事件發生在具有父元素的元素上(例如,在我們的例子中是 child 元素)時,現代瀏覽器執行兩個不同的階段 - 捕獲階段和冒泡階段。 在捕獲階段:

  • 瀏覽器檢查元素的最外層祖先<html>,是否在捕獲階段中註冊了一個onclick事件處理程式,如果是,則執行它。
  • 然後,它移動到<html>中單擊元素的下一個祖先元素,並執行相同的操作,然後是單擊元素再下一個祖先元素,依此類推,直到到達實際點選的元素。

在冒泡階段,恰恰相反:

  • 瀏覽器檢查實際點選的元素是否在冒泡階段中註冊了一個onclick事件處理程式,如果是,則執行它
  • 然後它移動到下一個直接的祖先元素,並做同樣的事情,然後是下一個,等等,直到它到達<html>元素。

這兩個階段如下圖所示:

bubbling-capturing

在現代瀏覽器中,預設情況下,所有事件處理程式都在冒泡階段進行註冊,這也是為什麼只有一個阻止冒泡方法的方法 event.stopPropagation(),而沒有阻止捕獲的方法,因為完全沒必要。

this 和 event.target

首先,我們得有一個清晰的認知:事件冒泡或者事件捕獲,都是針對註冊了事件的元素。

關於 this 和 event.target ,總結如下:

  • 在整個事件流程中,event.target 永遠都指向真正觸發了事件流程的元素 ,即處於事件觸階段的元素。
  • this 是正在執行事件的元素的引用,和 event.currentTarget 指向的元素是一致的,即當前執行的是哪個元素的監聽事件,this 和 event.currentTarget 指向的就是哪個元素。

event 還有一個屬性 event.srcElement,它是 event.target 的別名,但是是一個非標準屬性,儘量不在生產環境中使用。

阻止冒泡

假如有以下程式碼:

 parent.onclick = function1;
 child.onclick = function2;

當我們點選 child 時,由於事件預設會在冒泡階段註冊,所以,不僅會執行 function2,之後還會執行 function1,這樣的結果可能不是我們所期望的,我們更希望它們的點選事件之間互不影響。

如果要實現這點,只需要在 function2 中新增 event.stopPropagation() 即可。

擴充套件

現在我們將題目中的 JavaScript 程式碼再增加一份:

document.getElementById("parent").addEventListener("click", function (e) {
    alert(`parent 事件觸發,` + e.target.id);
}, false);

document.getElementById("child").addEventListener("click", function (e) {
    alert(`child 事件觸發,` + e.target.id);
}, true);

問題1:如果點選 child 元素,輸出是什麼?

問題2:如果點選 parent 元素,輸出是什麼?

可以看到,現在 parent 的點選事件是冒泡階段執行,child 的點選事件是在 捕獲階段執行。

針對問題1,由於 parent 註冊的是冒泡階段執行,所以它的事件是在 child 觸發階段後的冒泡階段執行的,所以答案應該是:先彈出 “child 事件觸發,child”,再彈出“parent 事件觸發,child”。

針對問題二,雖然 child 註冊的是捕獲階段執行事件,但是 parent 事件流程的捕獲根本走不到它,所以答案應該是:只彈出“parent 事件觸發,parent”。

總結

上面我們分析了這麼多,其實總結起來就下面幾條:

  • event.target 指向觸發事件流程的元素,且不會改變。
  • this 指向的是當前所執行事件的註冊元素。
  • 捕獲止於 event.target,冒泡始於 event.target。
  • 主流瀏覽器都預設在冒泡階段進行事件註冊,所以,只有阻止冒泡的方法而沒有阻止捕獲的方法。
  • 元素的 addEventListener 方法中的第三個引數是 true 或者 false,對元素自己觸發的事件流程都沒有任何影響,只有在它的父元素或者子元素在觸發相同的事件後才有影響。

小問題也有大根源,勇於發現,勇於探究!

~

~本文完,感謝閱讀!

~

學習有趣的知識,結識有趣的朋友,塑造有趣的靈魂!

大家好,我是〖程式設計三昧〗的作者 隱逸王,我的公眾號是『程式設計三昧』,歡迎關注,希望大家多多指教!

你來,懷揣期望,我有墨香相迎! 你歸,無論得失,唯以餘韻相贈!

知識與技能並重,內力和外功兼修,理論和實踐兩手都要抓、兩手都要硬!

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章