Javascript 創(chuàng)建自定義事件

2023-02-17 10:54 更新

我們不僅可以分配事件處理程序,還可以從 JavaScript 生成事件。

自定義事件可用于創(chuàng)建“圖形組件”。例如,我們自己的基于 JavaScript 的菜單的根元素可能會觸發(fā) open(打開菜單),select(有一項被選中)等事件來告訴菜單發(fā)生了什么。另一個代碼可能會監(jiān)聽事件,并觀察菜單發(fā)生了什么。

我們不僅可以生成出于自身目的而創(chuàng)建的全新事件,還可以生成例如 click 和 mousedown 等內(nèi)建事件。這可能會有助于自動化測試。

事件構(gòu)造器

內(nèi)建事件類形成一個層次結(jié)構(gòu)(hierarchy),類似于 DOM 元素類。根是內(nèi)建的 Event 類。

我們可以像這樣創(chuàng)建 Event 對象:

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

參數(shù):

  • type —— 事件類型,可以是像這樣 ?"click"? 的字符串,或者我們自己的像這樣 ?"my-event"? 的參數(shù)。
  • options —— 具有兩個可選屬性的對象:
    • ?bubbles: true/false? —— 如果為 ?true?,那么事件會冒泡。
    • ?cancelable: true/false? —— 如果為 ?true?,那么“默認(rèn)行為”就會被阻止。稍后我們會看到對于自定義事件,它意味著什么。
    • 默認(rèn)情況下,以上兩者都為 false:?{bubbles: false, cancelable: false}?。

dispatchEvent

事件對象被創(chuàng)建后,我們應(yīng)該使用 elem.dispatchEvent(event) 調(diào)用在元素上“運(yùn)行”它。

然后,處理程序會對它做出反應(yīng),就好像它是一個常規(guī)的瀏覽器事件一樣。如果事件是用 bubbles 標(biāo)志創(chuàng)建的,那么它會冒泡。

在下面這個示例中,click 事件是用 JavaScript 初始化創(chuàng)建的。處理程序工作方式和點(diǎn)擊按鈕的方式相同:

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

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

event.isTrusted

有一種方法可以區(qū)分“真實(shí)”用戶事件和通過腳本生成的事件。

對于來自真實(shí)用戶操作的事件,event.isTrusted 屬性為 true,對于腳本生成的事件,event.isTrusted 屬性為 false。

冒泡示例

我們可以創(chuàng)建一個名為 "hello" 的冒泡事件,并在 document 上捕獲它。

我們需要做的就是將 bubbles 設(shè)置為 true

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

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

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

  // 在 document 上的處理程序?qū)⒈患せ?,并顯示消息。

</script>

注意:

  1. 我們應(yīng)該對我們的自定義事件使用 ?addEventListener?,因?yàn)?nbsp;?on<event>? 僅存在于內(nèi)建事件中,?document.onhello? 則無法運(yùn)行。
  2. 必須設(shè)置 ?bubbles:true?,否則事件不會向上冒泡。

內(nèi)建事件(click)和自定義事件(hello)的冒泡機(jī)制相同。自定義事件也有捕獲階段和冒泡階段。

MouseEvent,KeyboardEvent 及其他

這是一個摘自于 UI 事件規(guī)范 的一個簡短的 UI 事件類列表:

  • ?UIEvent ?
  • ?FocusEvent ?
  • ?MouseEvent ?
  • ?WheelEvent ?
  • ?KeyboardEvent ?

如果我們想要創(chuàng)建這樣的事件,我們應(yīng)該使用它們而不是 new Event。例如,new MouseEvent("click")。

正確的構(gòu)造器允許為該類型的事件指定標(biāo)準(zhǔn)屬性。

就像鼠標(biāo)事件的 clientX/clientY 一樣:

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

alert(event.clientX); // 100

請注意:通用的 Event 構(gòu)造器不允許這樣做。

讓我們試試:

let event = new Event("click", {
  bubbles: true, // 構(gòu)造器 Event 中只有 bubbles 和 cancelable 可以工作
  cancelable: true,
  clientX: 100,
  clientY: 100
});

alert(event.clientX); // undefined,未知的屬性被忽略了!

從技術(shù)上講,我們可以通過在創(chuàng)建后直接分配 event.clientX=100 來解決這個問題。所以,這是一個方便和遵守規(guī)則的問題。瀏覽器生成的事件始終具有正確的類型。

規(guī)范中提供了不同 UI 事件的屬性的完整列表,例如 MouseEvent

自定義事件

對于我們自己的全新事件類型,例如 "hello",我們應(yīng)該使用 new CustomEvent。從技術(shù)上講,CustomEvent 和 Event 一樣。除了一點(diǎn)不同。

在第二個參數(shù)(對象)中,我們可以為我們想要與事件一起傳遞的任何自定義信息添加一個附加的屬性 detail

例如:

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

<script>
  // 事件附帶給處理程序的其他詳細(xì)信息
  elem.addEventListener("hello", function(event) {
    alert(event.detail.name);
  });

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

detail 屬性可以有任何數(shù)據(jù)。從技術(shù)上講,我們可以不用,因?yàn)槲覀兛梢栽趧?chuàng)建后將任何屬性分配給常規(guī)的 new Event 對象中。但是 CustomEvent 提供了特殊的 detail 字段,以避免與其他事件屬性的沖突。

此外,事件類描述了它是“什么類型的事件”,如果事件是自定義的,那么我們應(yīng)該使用 CustomEvent 來明確它是什么。

event.preventDefault()

許多瀏覽器事件都有“默認(rèn)行為”,例如,導(dǎo)航到鏈接,開始一個選擇,等。

對于新的,自定義的事件,絕對沒有默認(rèn)的瀏覽器行為,但是分派(dispatch)此類事件的代碼可能有自己的計劃,觸發(fā)該事件之后應(yīng)該做什么。

通過調(diào)用 event.preventDefault(),事件處理程序可以發(fā)出一個信號,指出這些行為應(yīng)該被取消。

在這種情況下,elem.dispatchEvent(event) 的調(diào)用會返回 false。那么分派(dispatch)該事件的代碼就會知道不應(yīng)該再繼續(xù)。

讓我們看一個實(shí)際的例子 —— 一只隱藏的兔子(可以是關(guān)閉菜單或者其他)。

在下面,你可以看到一個在其上分派了 "hide" 事件的 #rabbit 和 hide() 函數(shù),以使所有感興趣的各方面都知道這只兔子要隱藏起來。

任何處理程序都可以使用 rabbit.addEventListener('hide',...) 來監(jiān)聽該事件,并在需要時使用 event.preventDefault() 來取消該行為。然后兔子就不會藏起來了:

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

<script>
  function hide() {
    let event = new CustomEvent("hide", {
      cancelable: true // 沒有這個標(biāo)志,preventDefault 將不起作用
    });
    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 標(biāo)志,否則 event.preventDefault() 調(diào)用將會被忽略。

事件中的事件是同步的

通常事件是在隊列中處理的。也就是說:如果瀏覽器正在處理 onclick,這時發(fā)生了一個新的事件,例如鼠標(biāo)移動了,那么它的處理程序會被排入隊列,相應(yīng)的 mousemove 處理程序?qū)⒃?nbsp;onclick 事件處理完成后被調(diào)用。

值得注意的例外情況就是,一個事件是在另一個事件中發(fā)起的。例如使用 dispatchEvent。這類事件將會被立即處理,即在新的事件處理程序被調(diào)用之后,恢復(fù)到當(dāng)前的事件處理程序。

例如,在下面的代碼中,menu-open 事件是在 onclick 事件執(zhí)行過程中被調(diào)用的。

它會被立即執(zhí)行,而不必等待 onclick 處理程序結(jié)束:

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

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

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

    alert(2);
  };

  // 在 1 和 2 之間觸發(fā)
  document.addEventListener('menu-open', () => alert('nested'));
</script>

輸出順序?yàn)椋? → nested → 2。

請注意,嵌套事件 menu-open 會在 document 上被捕獲。嵌套事件的傳播(propagation)和處理先被完成,然后處理過程才會返回到外部代碼(onclick)。

這不只是與 dispatchEvent 有關(guān),還有其他情況。如果一個事件處理程序調(diào)用了觸發(fā)其他事件的方法 —— 它們同樣也會被以嵌套的方式同步處理。

不過有時候,這并不是我們期望的結(jié)果。我們想讓 onclick 不受 menu-open 或者其它嵌套事件的影響,優(yōu)先被處理完畢。

那么,我們就可以將 dispatchEvent(或另一個觸發(fā)事件的調(diào)用)放在 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>

現(xiàn)在,dispatchEvent 在當(dāng)前代碼執(zhí)行完成之后異步運(yùn)行,包括 menu.onclick,因此,事件處理程序是完全獨(dú)立的。

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

總結(jié)

要從代碼生成一個事件,我們首先需要創(chuàng)建一個事件對象。

通用的 Event(name, options) 構(gòu)造器接受任意事件名稱和具有兩個屬性的 options 對象:

  • 如果事件應(yīng)該冒泡,則 ?bubbles: true?。
  • 如果 ?event.preventDefault()? 應(yīng)該有效,則 ?cancelable: true?。

其他像 MouseEvent 和 KeyboardEvent 這樣的原生事件的構(gòu)造器,都接受特定于該事件類型的屬性。例如,鼠標(biāo)事件的 clientX。

對于自定義事件,我們應(yīng)該使用 CustomEvent 構(gòu)造器。它有一個名為 detail 的附加選項,我們應(yīng)該將事件特定的數(shù)據(jù)分配給它。然后,所有處理程序可以以 event.detail 的形式來訪問它。

盡管技術(shù)上可以生成像 click 或 keydown 這樣的瀏覽器事件,但我們還是應(yīng)謹(jǐn)慎使用它們。

我們不應(yīng)該生成瀏覽器事件,因?yàn)檫@是運(yùn)行處理程序的一種怪異(hacky)方式。大多數(shù)時候,這都是糟糕的架構(gòu)。

可以生成原生事件:

  • 如果第三方程序庫不提供其他交互方式,那么這是使第三方程序庫工作所需的一種骯臟手段。
  • 對于自動化測試,要在腳本中“點(diǎn)擊按鈕”并查看接口是否正確響應(yīng)。

使用我們自己的名稱的自定義事件通常是出于架構(gòu)的目的而創(chuàng)建的,以指示發(fā)生在菜單(menu),滑塊(slider),輪播(carousel)等內(nèi)部發(fā)生了什么。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號