preventDefault
和 stopPropagation
:使用時機和每個方法的確切內容。
Event.stopPropagation() 和 Event.preventDefault()
JavaScript 事件處理通常非常簡單。處理簡易 (相對平坦) HTML 結構時更是如此。不過,當事件透過元素階層進行移動 (或傳播) 時,情況會變得更加複雜。這通常是因為開發人員為 stopPropagation()
和/或 preventDefault()
解決問題,才能解決遇到的問題。如果您曾想過「我們只試用 preventDefault()
,如果這個方法無效,我就試試 stopPropagation()
;如果無效,我不妨試試這兩種方法」,就是想體驗一下這篇文章!我會詳細說明每種方法的用途、使用時機,並提供一些實用的範例,方便您探索。我的目標是要徹底解決這個困惑,
不過在深入說明之前,請務必簡單說明 JavaScript 中有兩種可能的事件處理方式 (在所有新式瀏覽器中,即第 9 版之前的 Internet Explorer 完全不支援事件擷取)。
事件樣式 (拍攝和冒泡)
所有新版瀏覽器都支援事件擷取功能,但開發人員很少使用這種做法。有趣的是,Netscape 最初曾支援這種攻擊方式。Netscape 最大的競爭對手 Microsoft Internet Explorer 完全不支援事件擷取,而是僅支援另一種名為「事件泡泡」的事件擷取方式。W3C 組成時,同時發現兩種事件樣式都適用,並透過向 addEventListener
方法的第三個參數宣告瀏覽器應同時支援兩者。該參數原本只是布林值,但所有新版瀏覽器都支援 options
物件做為第三個參數,方便您指定 (還有其他項目) 來指定 (以及其他項目),以便使用事件擷取功能:
someElement.addEventListener('click', myClickHandler, { capture: true | false });
請注意,options
為選用物件,如同其 capture
屬性。如果省略其中一項,capture
的預設值是 false
,表示會使用事件對話框。
事件擷取
如果事件處理常式是「監聽擷取階段」,代表什麼意思?為了瞭解這種情況 我們必須瞭解事件的來源和移動方式以下是「所有」事件的敘述,即使您是開發人員不會用到、重視或思考事件。
所有事件都會從視窗開始,並先經歷擷取階段。這表示調度事件時,系統會啟動視窗,並「先」朝「向下」移動目標元素。即使你只是在冒泡階段聆聽音樂。請參考以下標記和 JavaScript 範例:
<html>
<body>
<div id="A">
<div id="B">
<div id="C"></div>
</div>
</div>
</body>
</html>
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('#C was clicked');
},
true,
);
當使用者按一下元素 #C
時,系統會調度來自 window
的事件。此事件將透過其子系傳播,如下所示:
window
=> document
=> <html>
=> <body>
=> (以此類推),直到達成目標為止。
系統不會監聽 window
、document
、<html>
元素或 <body>
元素 (或是目標上的任何其他元素) 的點擊事件。事件仍會在 window
發出,並如上所述開始行程。
在本範例中,點擊事件會由各個元素從 window
和 #C
之間的各個元素傳播 (這是重要的字詞,因為這是 stopPropagation()
方法的直接關聯,本文件稍後將會說明) 從 window
到目標元素 (在本例中為 #C
)。
這表示點擊事件將於 window
開始,瀏覽器會詢問下列問題:
「是否在擷取階段監聽 window
的點擊事件?」如果是的話,系統就會觸發適當的事件處理常式。在此範例中,不會,因此不會觸發任何處理常式。
接下來,事件會傳播至 document
,瀏覽器會詢問:「在擷取階段,是否在監聽 document
上的點擊事件?」如果是的話,系統就會啟動適當的事件處理常式。
接下來,事件會傳播至 <html>
元素,瀏覽器會詢問:「在擷取階段,是否有監聽 <html>
元素的任何點擊?」如果答案為肯定,系統會觸發適當的事件處理常式。
接下來,事件會傳播至 <body>
元素,瀏覽器會詢問:「在擷取階段的 <body>
元素中,是否有任何監聽點擊事件?」如果答案為有效,系統就會觸發適當的事件處理常式。
接著,事件會傳播至 #A
元素。再次,瀏覽器會詢問:「是否在擷取階段監聽 #A
的點擊事件,如果會,就會觸發適當的事件處理常式。
接著,活動會傳播至 #B
元素,並詢問同一個問題。
最後,事件會到達目標,瀏覽器會詢問:「在擷取階段,是否在監聽 #C
元素的點擊事件?」這次答案是「當然!」事件位於目標的短暫時間,稱為「目標階段」。此時,事件處理常式會觸發,瀏覽器會 console.log「#C was 點選」然後結束。答錯了!我們並非一切完成。流程繼續進行,但現在改變了興奮階段。
活動泡泡
瀏覽器會詢問:
「是否有任何在準備階段監聽 #C
的點擊事件?」請密切留意這裡的說明。
您可以在「同時」和「對話框」中,完全監聽點擊 (或任何事件類型)。此外,如果您在這兩個階段中將事件處理常式串連起來 (例如:呼叫 .addEventListener()
兩次,一次使用 capture = true
,另一次使用 capture = false
),則系統會絕對針對同一個元素觸發兩個事件處理常式。但請特別注意,這些階段在不同階段 (一個在擷取階段和冒泡階段) 觸發。
接下來,事件會傳播 (較常被稱為「對話框」,原因是事件看起來似乎像是在 DOM 樹狀結構「向上」移動至其父項元素 #B
),而瀏覽器會詢問:「在討論階段,是否在監聽 #B
上的點擊事件是否有任何狀態?」在這個例子中,什麼都沒有,因此不會觸發任何處理常式。
接著,事件會對話框顯示 #A
,瀏覽器會詢問:「每當有監聽 #A
上的點擊事件嗎?」
接著,事件會對話框顯示「<body>
是否在準備階段的 <body>
元素上監聽點擊事件?」
接著,<html>
元素:「是否有任何項目會在對話框階段監聽 <html>
元素的點擊事件?
接著,document
:「是否有任何會在冒泡階段監聽 document
上的點擊事件?」
最後,window
:「是否有任何監聽使用者在工作階段中監聽視窗的點擊事件?」
大功告成!這是個漫長的旅程,現在我們的活動可能非常累人,但相信這次活動就是每個事件經歷的旅程!在多數情況下,這是從未註意到的情況,因為開發人員通常只關註一個事件階段,通常是另一個事件階段 (通常也是成長階段)。
值得花一些時間嘗試事件擷取與事件對話框,並在處理常式觸發時將一些筆記記錄到控制台。如此一來,就能查看事件發生的路徑,對你相當有意義。以下範例會監聽這兩個階段中的每個元素。
<html>
<body>
<div id="A">
<div id="B">
<div id="C"></div>
</div>
</div>
</body>
</html>
document.addEventListener(
'click',
function (e) {
console.log('click on document in capturing phase');
},
true,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
'click',
function (e) {
console.log('click on <html> in capturing phase');
},
true,
);
document.body.addEventListener(
'click',
function (e) {
console.log('click on <body> in capturing phase');
},
true,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('click on #A in capturing phase');
},
true,
);
document.getElementById('B').addEventListener(
'click',
function (e) {
console.log('click on #B in capturing phase');
},
true,
);
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('click on #C in capturing phase');
},
true,
);
document.addEventListener(
'click',
function (e) {
console.log('click on document in bubbling phase');
},
false,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
'click',
function (e) {
console.log('click on <html> in bubbling phase');
},
false,
);
document.body.addEventListener(
'click',
function (e) {
console.log('click on <body> in bubbling phase');
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('click on #A in bubbling phase');
},
false,
);
document.getElementById('B').addEventListener(
'click',
function (e) {
console.log('click on #B in bubbling phase');
},
false,
);
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('click on #C in bubbling phase');
},
false,
);
控制台輸出內容取決於您點選的元素。如果按一下 DOM 樹狀結構中的「最深」元素 (#C
元素),就會看到每個事件處理常式觸發的每個事件。我們已稍微調整 CSS 樣式,以便更清楚地顯示哪個元素,以下是主控台輸出的 #C
元素 (還有螢幕截圖):
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
"click on <body> in bubbling phase"
"click on <html> in bubbling phase"
"click on document in bubbling phase"
你可以在下方的現場示範中,透過互動方式玩遊戲。按一下 #C
元素,並觀察主控台輸出內容。
event.stopPropagation()
現在,我們瞭解事件來源,以及在擷取階段和準備階段透過 DOM 移動 (亦即傳播) 的方式,現在可以將注意力轉向 event.stopPropagation()
。
大多數原生 DOM 事件都可以呼叫 stopPropagation()
方法。因為有些呼叫這個方法不會有任何作用 (因為事件並不會從開始生效,所以請說「最」)。focus
、blur
、load
、scroll
等某些事件屬於這個類別。您可以呼叫 stopPropagation()
,但由於這些事件不會傳播,因此不會發生任何過程。
但「stopPropagation
」有哪些功能?
這幾乎就是上面所說的。呼叫此方法時,事件會從該時間點停止傳播至其他會前往的任何元素。「兩個」方向 (拍攝和對話框) 都一樣。因此,如果您在擷取階段的任何位置呼叫 stopPropagation()
,該事件絕不會進入目標階段或冒泡階段。如果您在準備階段呼叫此方法,就已經完成擷取階段,但會從您呼叫該階段的時間點停止「向上」。
回到相同的範例標記,如果我們在 #B
元素的擷取階段呼叫 stopPropagation()
,您覺得會發生什麼情況?
這會產生下列輸出內容:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
你可以在下方的現場示範中,透過互動方式玩遊戲。在即時示範中按一下 #C
元素,並觀察主控台輸出內容。
可以在冒泡階段停止在 #A
停止傳播嗎?這會產生下列輸出內容:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
你可以在下方的現場示範中,透過互動方式玩遊戲。在即時示範中按一下 #C
元素,並觀察主控台輸出內容。
還有一個,只是樂趣無窮。如果在 #C
的目標階段中呼叫 stopPropagation()
,會發生什麼情況?提醒您,「目標階段」是指事件抵達目標的期間名稱。這會產生下列輸出內容:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
請注意,我們記錄「在擷取階段中按一下 #C」的 #C
事件處理常式「仍然」執行,但記錄「在對話階段中點選 #C」的事件處理常式不會。這應該是很合理的。我們從前者呼叫了 stopPropagation()
,因此是事件傳播的停止時間。
你可以在下方的現場示範中,透過互動方式玩遊戲。在即時示範中按一下 #C
元素,並觀察主控台輸出內容。
任何現場示範影片都鼓勵大家多方嘗試。請嘗試只點選 #A
元素,或只點選 body
元素。請嘗試預測會發生什麼情況,然後再觀察是否正確。現在,您應該能夠準確預測。
event.stopImmediatePropagation()
請問這是什麼怪異或不好用的方法?這種做法與 stopPropagation
類似,但不會停止將事件導向子系 (擷取) 或祖系 (對話框),只有在將多個事件處理常式連接至單一元素時。由於 addEventListener()
支援事件的多點傳播樣式,因此可將事件處理常式連接至單一元素多次。發生這種情況時 (大部分的瀏覽器都會按照連接時的順序執行事件處理常式)。呼叫 stopImmediatePropagation()
可防止任何後續處理常式觸發。請參考以下範例:
<html>
<body>
<div id="A">I am the #A element</div>
</body>
</html>
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I shall run first!');
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I shall run second!');
e.stopImmediatePropagation();
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I would have run third, if not for stopImmediatePropagation');
},
false,
);
上述範例會產生下列主控台輸出內容:
"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"
請注意,第三個事件處理常式一律不會執行,因為第二個事件處理常式會呼叫 e.stopImmediatePropagation()
。如果我們改為呼叫 e.stopPropagation()
,第三個處理常式仍會執行。
event.preventDefault()
如果 stopPropagation()
阻止事件朝「向下」(擷取) 或「向上」(對話框) 移動,preventDefault()
會有什麼影響?聽起來好像是這樣:這嗎?
算不上是。雖然兩者經常令人混淆,但實際上彼此間並沒有什麼好處。看到「preventDefault()
」時,在頭中加入「action」這個字詞。請思考「防止預設動作」
你可能會詢問哪些預設動作?遺憾的是,這個問題並不明確,因為嚴重取決於相關的元素 + 事件組合。更令人困惑的是 有時根本沒有預設動作!
我們先從簡單的範例開始。按一下網頁上的連結後
會發生什麼情況?很明顯,您預期瀏覽器會前往該連結指定的網址。在這個範例中,元素是錨定標記,而事件則是點擊事件。該組合 (<a>
+ click
) 具有前往連結 href 的「預設動作」。如果要「防止」瀏覽器執行這項預設動作,該怎麼辦?也就是說,假設您要禁止瀏覽器前往 <a>
元素的 href
屬性指定的網址嗎?這就是 preventDefault()
會為您執行的操作。請參閱以下範例:
<a id="avett" href="https://www.theavettbrothers.com/welcome">The Avett Brothers</a>
document.getElementById('avett').addEventListener(
'click',
function (e) {
e.preventDefault();
console.log('Maybe we should just play some of their music right here instead?');
},
false,
);
你可以在下方的現場示範中,透過互動方式玩遊戲。按一下「The Avett Brothers」連結,查看主控台輸出內容 (還有您未重新導向至 Avett Brothers 網站)。
通常,按一下標示著 The Avett Brothers 的連結會前往 www.theavettbrothers.com
。不過,我們已將點擊事件處理常式連結至 <a>
元素,並指定要禁止預設動作。因此,使用者點選這個連結時不得前往任何頁面,控制台只會記錄「或許我們應該直接在這裡播放音樂嗎?」
你還能利用哪些其他元素/事件組合防止特定動作?我無法將所有內容一一列出 有時您只是要做實驗看看以下簡要說明其中幾項:
<form>
元素 +「提交」事件:如果將這個組合設為preventDefault()
,則會導致表單無法提交。如要執行驗證作業,一旦發現錯誤,可以有條件地呼叫 preventDefault,防止表單提交。<a>
元素 + 「點擊」事件:這個組合的preventDefault()
會防止瀏覽器前往<a>
元素 href 屬性中指定的網址。document
+ 「滑鼠滾輪」事件:針對此組合,preventDefault()
可避免使用滑鼠滾輪捲動頁面 (但使用鍵盤捲動頁面仍會正常運作)。
↜ 需要使用{ passive: false }
呼叫addEventListener()
。document
+ 「keydown」事件:這個組合的preventDefault()
代表致命。這種方式會轉譯網頁,幾乎不需要使用,可防止鍵盤捲動、按 Tab 鍵和鍵盤醒目顯示功能。document
+ 「mousedown」事件:針對這個組合,preventDefault()
會避免使用滑鼠醒目顯示文字,以及使用滑鼠向下叫用的任何其他「預設」動作。<input>
元素 + 「keypress」事件:針對此組合,preventDefault()
會防止使用者輸入的字元到達輸入元素 (但切勿這麼做,而且在極少數的情況下是正當原因)。document
+「內容選單」事件:針對這個組合,preventDefault()
會防止使用者按一下滑鼠右鍵或長按 (或其他內容選單可能顯示的方式) 時顯示原生瀏覽器的內容選單。
以上清單僅列舉部分方式,希望您大致瞭解 preventDefault()
的使用方式。
想聽個有趣的笑話嗎?
如果在擷取階段 stopPropagation()
「和」preventDefault()
,從文件開始會怎麼樣?嗨翻來!下列程式碼片段會轉譯幾乎完全無用的網頁:
function preventEverything(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
document.addEventListener('click', preventEverything, true);
document.addEventListener('keydown', preventEverything, true);
document.addEventListener('mousedown', preventEverything, true);
document.addEventListener('contextmenu', preventEverything, true);
document.addEventListener('mousewheel', preventEverything, { capture: true, passive: false });
我不曉得為什麼要這麼做 (除非你可能會玩笑話除外),但考慮到底發生了什麼也很有幫助,也瞭解到底會是什麼緣由。
所有事件都源自 window
,因此在這個程式碼片段中,我們將停止並停止在其測試群組、所有 click
、keydown
、mousedown
、contextmenu
和 mousewheel
事件中,前往任何可能監聽這些事件的元素。我們也會呼叫 stopImmediatePropagation
,讓在此文件之後接上文件的任何處理常式都會遭到阻斷。
請注意,stopPropagation()
和 stopImmediatePropagation()
並非 (至少) 會導致頁面無用的結果。只是防止事件取得其他目的地。
不過,我們也會呼叫 preventDefault()
,提醒您,系統不會執行預設動作。因此,所有預設動作 (例如滑鼠滾輪捲動、鍵盤捲動或醒目顯示或 Tab 鍵、連結點擊、內容選單顯示等) 都會受到阻止,導致頁面處於無使用狀態。
現場示範
如要在同一處再次探索本文中的所有範例,請查看下方的嵌入式示範。
特別銘謝
Tom Wilson 在 Unsplash 上提供的主頁橫幅。