2022 年 10 月 14 日

發送自訂事件

我們不僅可以指定處理常式,還可以從 JavaScript 產生事件。

自訂事件可以用來建立「圖形元件」。例如,我們自己的 JS-based 選單的根元素可能會觸發事件,說明選單發生了什麼事:open(選單開啟)、select(選取項目)等等。其他程式碼可能會偵聽事件並觀察選單發生了什麼事。

我們不僅可以產生我們自己發明的新事件,還可以產生內建事件,例如 clickmousedown 等。這可能有助於自動化測試。

事件建構函式

內建事件類別形成一個階層,類似 DOM 元素類別。根目錄是內建的 Event 類別。

我們可以這樣建立 Event 物件

let event = new Event(type[, options]);

引數

  • type – 事件類型,例如 "click" 或我們自訂的 "my-event" 等字串。

  • options – 包含兩個選用屬性的物件

    • bubbles: true/false – 若為 true,則事件會冒泡。
    • cancelable: true/false – 若為 true,則可以防止「預設動作」。稍後我們會了解這對自訂事件的意義。

    預設兩者皆為 false:{bubbles: false, cancelable: false}

dispatchEvent

建立事件物件後,我們應該使用呼叫 elem.dispatchEvent(event) 在元素上「執行」它。

然後處理常式會對它做出反應,就像它是一般瀏覽器事件一樣。如果事件是用 bubbles 旗標建立的,則它會冒泡。

在以下範例中,click 事件在 JavaScript 中啟動。處理常式的工作方式就像按鈕被點擊一樣

<button id="elem" onclick="alert('Click!');">Autoclick</button>

<script>
  let event = new Event("click");
  elem.dispatchEvent(event);
</script>
event.isTrusted

有一種方法可以區分「真實」使用者事件和指令碼產生的事件。

屬性 event.isTrusted 對來自真實使用者動作的事件為 true,對指令碼產生的事件為 false

冒泡範例

我們可以用名稱 "hello" 建立一個冒泡事件,並在 document 上擷取它。

我們只需要將 bubbles 設定為 true

<h1 id="elem">Hello from the script!</h1>

<script>
  // catch on document...
  document.addEventListener("hello", function(event) { // (1)
    alert("Hello from " + event.target.tagName); // Hello from H1
  });

  // ...dispatch on elem!
  let event = new Event("hello", {bubbles: true}); // (2)
  elem.dispatchEvent(event);

  // the handler on document will activate and display the message.

</script>

注意事項

  1. 我們應該對自訂事件使用 addEventListener,因為 on<event> 只存在於內建事件,document.onhello 無效。
  2. 必須設定 bubbles:true,否則事件不會冒泡。

冒泡機制對內建 (click) 和自訂 (hello) 事件相同。也有擷取和冒泡階段。

MouseEvent、KeyboardEvent 等

以下是 UI 事件規格 中 UI 事件類別的簡短清單

  • UIEvent
  • FocusEvent
  • MouseEvent
  • WheelEvent
  • KeyboardEvent

如果我們想建立此類事件,應使用這些事件,而不是 new Event。例如,new MouseEvent("click")

正確的建構函數允許為該類型的事件指定標準屬性。

例如,滑鼠事件的 clientX/clientY

let event = new MouseEvent("click", {
  bubbles: true,
  cancelable: true,
  clientX: 100,
  clientY: 100
});

alert(event.clientX); // 100

請注意:一般性的 Event 建構函數不允許這樣做。

讓我們試試看

let event = new Event("click", {
  bubbles: true, // only bubbles and cancelable
  cancelable: true, // work in the Event constructor
  clientX: 100,
  clientY: 100
});

alert(event.clientX); // undefined, the unknown property is ignored!

技術上,我們可以在建立後直接指定 event.clientX=100 來解決這個問題。因此,這是一個方便性和遵循規則的問題。瀏覽器產生的事件始終具有正確的類型。

不同 UI 事件的完整屬性清單在規格中,例如,MouseEvent

自訂事件

對於我們自己的全新事件類型,例如 "hello",我們應該使用 new CustomEvent。技術上,CustomEventEvent 相同,只有一個例外。

在第二個參數(物件)中,我們可以新增一個額外的屬性 detail,用於傳遞我們想隨事件傳遞的任何自訂資訊。

例如

<h1 id="elem">Hello for John!</h1>

<script>
  // additional details come with the event to the handler
  elem.addEventListener("hello", function(event) {
    alert(event.detail.name);
  });

  elem.dispatchEvent(new CustomEvent("hello", {
    detail: { name: "John" }
  }));
</script>

detail 屬性可以包含任何資料。技術上,我們可以不用它,因為我們可以在建立常規 new Event 物件後將任何屬性指定給它。但是,CustomEvent 提供了特殊的 detail 欄位,以避免與其他事件屬性發生衝突。

此外,事件類別描述了「事件的種類」,如果事件是自訂的,那麼我們應該使用 CustomEvent,以清楚說明事件的種類。

event.preventDefault()

許多瀏覽器事件都有「預設動作」,例如導覽至連結、開始選取等等。

對於新的自訂事件,絕對沒有預設的瀏覽器動作,但是觸發事件的程式碼可能會在觸發事件後有自己的計畫要執行。

透過呼叫 event.preventDefault(),事件處理常式可以發出一個訊號,表示應取消這些動作。

在這種情況下,呼叫 elem.dispatchEvent(event) 會傳回 false。而觸發事件的程式碼知道它不應該繼續執行。

讓我們來看一個實際的範例 - 隱藏兔子(可能是關閉選單或其他東西)。

下方你可以看到一個 #rabbithide() 函式,它會在函式中發送 "hide" 事件,讓所有有興趣的方知道兔子將要躲起來。

任何處理常式都可以使用 rabbit.addEventListener('hide',...) 聆聽該事件,並且在需要時使用 event.preventDefault() 取消動作。這樣兔子就不會消失。

<pre id="rabbit">
  |\   /|
   \|_|/
   /. .\
  =\_Y_/=
   {>o<}
</pre>
<button onclick="hide()">Hide()</button>

<script>
  function hide() {
    let event = new CustomEvent("hide", {
      cancelable: true // without that flag preventDefault doesn't work
    });
    if (!rabbit.dispatchEvent(event)) {
      alert('The action was prevented by a handler');
    } else {
      rabbit.hidden = true;
    }
  }

  rabbit.addEventListener('hide', function(event) {
    if (confirm("Call preventDefault?")) {
      event.preventDefault();
    }
  });
</script>

請注意:事件必須有 cancelable: true 旗標,否則 event.preventDefault() 呼叫會被忽略。

事件中的事件是同步的

事件通常會在佇列中處理。也就是說:如果瀏覽器正在處理 onclick,而新的事件發生,例如滑鼠移動,那麼其處理會排隊,對應的 mousemove 處理常式會在 onclick 處理完成後呼叫。

值得注意的例外情況是,當一個事件從另一個事件中啟動時,例如使用 dispatchEvent。此類事件會立即處理:新的事件處理常式會被呼叫,然後目前的事件處理會恢復。

例如,在以下程式碼中,menu-open 事件會在 onclick 期間觸發。

它會立即處理,而不用等到 onclick 處理常式結束

<button id="menu">Menu (click me)</button>

<script>
  menu.onclick = function() {
    alert(1);

    menu.dispatchEvent(new CustomEvent("menu-open", {
      bubbles: true
    }));

    alert(2);
  };

  // triggers between 1 and 2
  document.addEventListener('menu-open', () => alert('nested'));
</script>

輸出的順序是:1 → 巢狀 → 2。

請注意,巢狀事件 menu-open 會在 document 上被捕捉。巢狀事件的傳播和處理會在處理回到外部程式碼 (onclick) 之前完成。

這不只與 dispatchEvent 有關,還有其他情況。如果事件處理常式呼叫會觸發其他事件的方法,它們也會以巢狀方式同步處理。

假設我們不喜歡這樣。我們希望 onclick 能夠先完全處理,獨立於 menu-open 或任何其他巢狀事件。

然後我們可以將 dispatchEvent (或其他觸發事件的呼叫) 放在 onclick 的最後面,或者,也許更好的是,將它包在零延遲的 setTimeout

<button id="menu">Menu (click me)</button>

<script>
  menu.onclick = function() {
    alert(1);

    setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
      bubbles: true
    })));

    alert(2);
  };

  document.addEventListener('menu-open', () => alert('nested'));
</script>

現在 dispatchEvent 會在目前的程式碼執行完成後非同步執行,包括 menu.onclick,因此事件處理常式是完全分開的。

輸出順序變為:1 → 2 → nested。

摘要

要從程式碼產生事件,我們首先需要建立一個事件物件。

通用的 Event(name, options) 建構函式接受一個任意的事件名稱和具有兩個屬性的 options 物件

  • 如果事件應該冒泡,則為 bubbles: true
  • 如果 event.preventDefault() 應該運作,則為 cancelable: true

其他原生事件的建構函式,例如 MouseEventKeyboardEvent 等,接受特定於該事件類型的屬性。例如,滑鼠事件的 clientX

對於自訂事件,我們應該使用 CustomEvent 建構函式。它有一個名為 detail 的額外選項,我們應該將事件特定資料指定給它。然後所有處理常式都可以將其存取為 event.detail

儘管有產生瀏覽器事件(例如 clickkeydown)的技術可能性,但我們應該非常小心地使用它們。

我們不應該產生瀏覽器事件,因為這是一種執行處理常式的駭客方式。這在大部分時間都是糟糕的架構。

原生事件可能會產生

  • 作為一種骯髒的駭客方式,讓第三方程式庫以所需的方式運作,如果它們沒有提供其他互動方式。
  • 對於自動化測試,在指令碼中「按一下按鈕」並查看介面是否正確反應。

具有我們自己名稱的自訂事件通常會為架構目的而產生,以表示我們的選單、滑塊、輪播等內部發生了什麼事。

教學課程地圖

留言

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