2022 年 1 月 21 日

瀏覽器預設動作

許多事件會自動導致瀏覽器執行某些動作。

例如

  • 點擊連結 – 啟動導覽至其 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 預設為 touchstarttouchmove 事件的 true

event.defaultPrevented

如果預設動作已禁止,則屬性 event.defaultPreventedtrue,否則為 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()

正如我們清楚看到的,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> 指定的處理常式。

addEventListenerpassive: true 選項告訴瀏覽器不會防止動作。這對於某些行動裝置事件很有用,例如 touchstarttouchmove,以告訴瀏覽器它不應該等到所有處理常式都完成才捲動。

如果預設動作被阻止,event.defaultPrevented 的值會變成 true,否則會是 false

保持語意,不要濫用

技術上來說,透過阻止預設動作並加入 JavaScript,我們可以自訂任何元素的行為。例如,我們可以讓連結 <a> 像按鈕一樣運作,讓按鈕 <button> 像連結一樣運作(重新導向到其他網址)。

但我們通常應該保留 HTML 元素的語意意義。例如,<a> 應該執行導覽,而不是按鈕。

除了「這是一件好事」之外,這也能讓你的 HTML 在無障礙方面更完善。

另外,如果我們考慮 <a> 的範例,請注意:瀏覽器允許我們在新視窗中開啟此類連結(透過右鍵按一下它們或其他方式)。而且人們喜歡這樣。但如果我們使用 JavaScript 讓按鈕像連結一樣運作,甚至使用 CSS 讓它看起來像連結,那麼 <a> 特有的瀏覽器功能仍然無法對它運作。

任務

重要性:3

為什麼在下列程式碼中 return false 完全不起作用?

<script>
  function handler() {
    alert( "..." );
    return false;
  }
</script>

<a href="https://w3.org" onclick="handler()">the browser will go to w3.org</a>

瀏覽器在按一下時會追蹤網址,但我們不想要這樣。

如何修復?

當瀏覽器讀取 on* 屬性(例如 onclick)時,它會從其內容建立處理常式。

對於 onclick="handler()",函式會是

function(event) {
  handler() // the content of onclick
}

現在我們可以看到 handler() 回傳的值並未被使用,而且不會影響結果。

修復方法很簡單

<script>
  function handler() {
    alert("...");
    return false;
  }
</script>

<a href="https://w3.org" onclick="return handler()">w3.org</a>

我們也可以使用 event.preventDefault(),如下所示

<script>
  function handler(event) {
    alert("...");
    event.preventDefault();
  }
</script>

<a href="https://w3.org" onclick="handler(event)">w3.org</a>
重要性:5

id="contents" 元素中的所有連結詢問使用者是否真的要離開。如果他們不離開,就不要追蹤。

像這樣

詳細資料

  • 元素中的 HTML 可能隨時動態載入或重新產生,因此我們無法找到所有連結並在它們上面放置處理常式。使用事件委派。
  • 內容可能包含巢狀標籤。連結內部也可能包含,例如 <a href=".."><i>...</i></a>

為任務開啟沙盒。

這是事件委派模式的絕佳用法。

在實際應用中,我們可以傳送「記錄」要求到伺服器,而不是詢問使用者,以儲存訪客離開位置的資訊。或者,我們可以載入內容並直接在頁面上顯示(如果允許的話)。

我們只需要攔截 contents.onclick 並使用 confirm 詢問使用者。一個好主意是使用 link.getAttribute('href') 而不是 link.href 來取得網址。有關詳細資料,請參閱解決方案。

在沙盒中開啟解決方案。

重要性:5

建立一個圖片庫,其中主圖片會隨著點擊縮圖而改變。

像這樣

P.S. 使用事件委派。

為任務開啟沙盒。

解決方案是將處理常式指派給容器並追蹤點擊。如果點擊在 <a> 連結上,則將 #largeImgsrc 變更為縮圖的 href

在沙盒中開啟解決方案。

教學地圖

評論

在評論前請先閱讀…
  • 如果您有建議要改進 - 請 提交 GitHub 問題 或提交拉取請求,而不是留言。
  • 如果您無法理解文章中的某些內容 - 請說明。
  • 要插入幾行程式碼,請使用 <code> 標籤,對於多行 - 將它們包在 <pre> 標籤中,對於超過 10 行 - 使用沙盒 (plnkrjsbincodepen…)