最近我在做前端面試題總結系列,感興趣的朋友可以新增關注,歡迎指正、交流。
爭取每個知識點能夠多總結一些,至少要做到在面試時,針對每個知識點都可以侃起來,不至於啞火。
前言
在上一篇文章【前端 · 面試 】JavaScript 之你不一定會的基礎題(一)中,有同學產生了這樣一個疑惑:為什麼 click 事件的監聽函式中,this.id
和 event.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 元素的事件執行順序一般有三個階段:
- 事件捕獲
- 事件觸發
- 事件冒泡
整個過程如下圖:
事件捕獲和事件冒泡
當一個事件發生在具有父元素的元素上(例如,在我們的例子中是 child 元素)時,現代瀏覽器執行兩個不同的階段 - 捕獲階段和冒泡階段。 在捕獲階段:
- 瀏覽器檢查元素的最外層祖先
<html>
,是否在捕獲階段中註冊了一個onclick
事件處理程式,如果是,則執行它。 - 然後,它移動到
<html>
中單擊元素的下一個祖先元素,並執行相同的操作,然後是單擊元素再下一個祖先元素,依此類推,直到到達實際點選的元素。
在冒泡階段,恰恰相反:
- 瀏覽器檢查實際點選的元素是否在冒泡階段中註冊了一個
onclick
事件處理程式,如果是,則執行它 - 然後它移動到下一個直接的祖先元素,並做同樣的事情,然後是下一個,等等,直到它到達
<html>
元素。
這兩個階段如下圖所示:
在現代瀏覽器中,預設情況下,所有事件處理程式都在冒泡階段進行註冊,這也是為什麼只有一個阻止冒泡方法的方法
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 協議》,轉載必須註明作者和本文連結