許多事件會自動導致瀏覽器執行某些動作。
例如
- 點擊連結 – 啟動導覽至其 URL。
- 點擊表單送出按鈕 – 啟動送出至伺服器。
- 在文字上按下滑鼠按鈕並移動 – 選取文字。
如果我們在 JavaScript 中處理事件,我們可能不希望發生對應的瀏覽器動作,並希望改為實作其他行為。
防止瀏覽器動作
有兩種方式可以告訴瀏覽器我們不希望它執行動作
- 主要的方式是使用
event
物件。有一個方法event.preventDefault()
。 - 如果處理常式使用
on<event>
指定(而非使用addEventListener
),那麼傳回false
也會產生相同的效果。
在此 HTML 中,按一下連結不會導致導覽;瀏覽器不會執行任何動作
<a href="/" onclick="return false">Click here</a>
or
<a href="/" onclick="event.preventDefault()">here</a>
在下一範例中,我們將使用此技術來建立一個由 JavaScript 提供動力的選單。
false
是例外事件處理常式傳回的值通常會被忽略。
唯一的例外是從使用 on<event>
指定的處理常式傳回 false
。
在所有其他情況下,都會忽略 return
值。特別是,傳回 true
沒有意義。
範例:選單
考慮一個網站選單,如下所示
<ul id="menu" class="menu">
<li><a href="/html">HTML</a></li>
<li><a href="/javascript">JavaScript</a></li>
<li><a href="/css">CSS</a></li>
</ul>
以下是它使用一些 CSS 的樣貌
選單項目實作為 HTML 連結 <a>
,而非按鈕 <button>
。有幾個這樣做的理由,例如
- 許多人喜歡使用「右鍵按一下」-「在新視窗中開啟」。如果我們使用
<button>
或<span>
,這將無法運作。 - 搜尋引擎會在編製索引時追蹤
<a href="...">
連結。
因此我們在標記中使用 <a>
。但我們通常打算在 JavaScript 中處理按一下動作。因此我們應該防止瀏覽器的預設動作。
如下所示
menu.onclick = function(event) {
if (event.target.nodeName != 'A') return;
let href = event.target.getAttribute('href');
alert( href ); // ...can be loading from the server, UI generation etc
return false; // prevent browser action (don't go to the URL)
};
如果我們省略 return false
,那麼在我們的程式碼執行後,瀏覽器將執行其「預設動作」- 導覽至 href
中的 URL。而我們在此不需要這樣做,因為我們自己處理按一下動作。
順帶一提,在此使用事件委派會讓我們的選單非常靈活。我們可以加入巢狀清單,並使用 CSS 為它們設定「滑動展開」的樣式。
某些事件會一個接著一個發生。如果我們防止第一個事件,就不會有第二個事件。
例如,<input>
欄位的 mousedown
會導致焦點移至該欄位,並觸發 focus
事件。如果我們禁止 mousedown
事件,就不會有焦點。
嘗試點擊下方第一個 <input>
,會觸發 focus
事件。但如果你點擊第二個,則不會有焦點。
<input value="Focus works" onfocus="this.value=''">
<input onmousedown="return false" onfocus="this.value=''" value="Click me">
這是因為瀏覽器動作已在 mousedown
時取消。如果我們使用其他方式輸入,仍然可以聚焦。例如,使用 Tab 鍵從第一個輸入切換到第二個輸入。但無法再使用滑鼠點擊。
「被動」處理常式選項
addEventListener
的選用 passive: true
選項會向瀏覽器發出訊號,表示處理常式不會呼叫 preventDefault()
。
為什麼需要這樣做?
有些事件,例如行動裝置上的 touchmove
(當使用者在螢幕上移動手指時),預設會導致捲動,但可以在處理常式中使用 preventDefault()
來禁止捲動。
因此,當瀏覽器偵測到此類事件時,必須先處理所有處理常式,然後如果任何地方都沒有呼叫 preventDefault
,則可以繼續捲動。這可能會導致 UI 發生不必要的延遲和「抖動」。
passive: true
選項會告訴瀏覽器,處理常式不會取消捲動。然後,瀏覽器會立即捲動,提供最流暢的體驗,同時處理事件。
對於某些瀏覽器(Firefox、Chrome),passive
預設為 touchstart
和 touchmove
事件的 true
。
event.defaultPrevented
如果預設動作已禁止,則屬性 event.defaultPrevented
為 true
,否則為 false
。
它有一個有趣的用例。
還記得在章節 冒泡和擷取 中,我們討論過 event.stopPropagation()
,以及為什麼禁止冒泡很糟糕?
有時我們可以使用 event.defaultPrevented
來代替,以向其他事件處理常式發出訊號,表示事件已處理。
讓我們來看一個實際範例。
預設情況下,瀏覽器在 contextmenu
事件(滑鼠右鍵點擊)時會顯示包含標準選項的內容選單。我們可以禁止它並顯示自己的內容選單,如下所示
<button>Right-click shows browser context menu</button>
<button oncontextmenu="alert('Draw our menu'); return false">
Right-click shows our context menu
</button>
現在,除了該內容選單之外,我們還想實作文件範圍的內容選單。
右鍵點擊時,應顯示最近的內容選單。
<p>Right-click here for the document context menu</p>
<button id="elem">Right-click here for the button context menu</button>
<script>
elem.oncontextmenu = function(event) {
event.preventDefault();
alert("Button context menu");
};
document.oncontextmenu = function(event) {
event.preventDefault();
alert("Document context menu");
};
</script>
問題在於,當我們點擊 elem
時,會出現兩個選單:按鈕層級和(事件冒泡)文件層級選單。
如何修復?其中一個解決方案是這樣思考:「當我們在按鈕處理常式中處理右鍵點擊時,讓我們停止它的冒泡」並使用 event.stopPropagation()
<p>Right-click for the document menu</p>
<button id="elem">Right-click for the button menu (fixed with event.stopPropagation)</button>
<script>
elem.oncontextmenu = function(event) {
event.preventDefault();
event.stopPropagation();
alert("Button context menu");
};
document.oncontextmenu = function(event) {
event.preventDefault();
alert("Document context menu");
};
</script>
現在按鈕層級的選單按預期運作。但代價很高。我們永遠拒絕任何外部程式碼存取右鍵點擊的資訊,包括收集統計資料的計數器等等。這相當不明智。
替代解決方案是在 document
處理常式中檢查預設動作是否已防止?如果是,則事件已處理,我們不需要對它做出反應。
<p>Right-click for the document menu (added a check for event.defaultPrevented)</p>
<button id="elem">Right-click for the button menu</button>
<script>
elem.oncontextmenu = function(event) {
event.preventDefault();
alert("Button context menu");
};
document.oncontextmenu = function(event) {
if (event.defaultPrevented) return;
event.preventDefault();
alert("Document context menu");
};
</script>
現在一切都運作正常。如果我們有巢狀元素,且每個元素都有自己的內容選單,那也會運作。只要確保在每個 contextmenu
處理常式中檢查 event.defaultPrevented
。
正如我們清楚看到的,event.stopPropagation()
和 event.preventDefault()
(也稱為 return false
)是兩件不同的事情。它們彼此無關。
還有其他實作巢狀內容選單的方法。其中一種方法是有一個單一的全域物件,其中包含 document.oncontextmenu
的處理常式,以及允許我們在其中儲存其他處理常式的函式。
該物件將擷取任何右鍵點擊,瀏覽儲存的處理常式並執行適當的處理常式。
但接著,想要內容選單的每個程式碼片段都應該知道該物件,並使用它的協助,而不是自己的 contextmenu
處理常式。
摘要
有很多預設瀏覽器動作
mousedown
– 開始選取(移動滑鼠以選取)。- 在
<input type="checkbox">
上click
– 勾選/取消勾選input
。 submit
– 按一下<input type="submit">
或在表單欄位中按下 Enter 會導致此事件發生,瀏覽器會在之後提交表單。keydown
– 按下按鍵可能會導致在欄位中加入字元,或其他動作。contextmenu
– 事件發生在右鍵點擊時,動作是顯示瀏覽器內容選單。- …還有更多…
如果我們想要透過 JavaScript 獨家處理事件,則可以防止所有預設動作。
若要防止預設動作,請使用 event.preventDefault()
或 return false
。第二個方法僅適用於使用 on<event>
指定的處理常式。
addEventListener
的 passive: true
選項告訴瀏覽器不會防止動作。這對於某些行動裝置事件很有用,例如 touchstart
和 touchmove
,以告訴瀏覽器它不應該等到所有處理常式都完成才捲動。
如果預設動作被阻止,event.defaultPrevented
的值會變成 true
,否則會是 false
。
技術上來說,透過阻止預設動作並加入 JavaScript,我們可以自訂任何元素的行為。例如,我們可以讓連結 <a>
像按鈕一樣運作,讓按鈕 <button>
像連結一樣運作(重新導向到其他網址)。
但我們通常應該保留 HTML 元素的語意意義。例如,<a>
應該執行導覽,而不是按鈕。
除了「這是一件好事」之外,這也能讓你的 HTML 在無障礙方面更完善。
另外,如果我們考慮 <a>
的範例,請注意:瀏覽器允許我們在新視窗中開啟此類連結(透過右鍵按一下它們或其他方式)。而且人們喜歡這樣。但如果我們使用 JavaScript 讓按鈕像連結一樣運作,甚至使用 CSS 讓它看起來像連結,那麼 <a>
特有的瀏覽器功能仍然無法對它運作。
評論
<code>
標籤,對於多行 - 將它們包在<pre>
標籤中,對於超過 10 行 - 使用沙盒 (plnkr、jsbin、codepen…)