2022 年 10 月 14 日

分派自定义事件

我们不仅可以分配处理程序,还可以从 JavaScript 生成事件。

自定义事件可用于创建“图形组件”。例如,我们自己的基于 JS 的菜单的根元素可能会触发事件,告诉菜单发生了什么: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.isTrustedtrue,对于脚本生成的事件,则为 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-opendocument 上被捕获。嵌套事件的传播和处理在处理返回到外部代码(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 → 嵌套。

总结

要从代码生成事件,我们首先需要创建一个事件对象。

通用 Event(name, options) 构造函数接受任意事件名称和具有两个属性的 options 对象

  • 如果事件应该冒泡,则 bubbles: true
  • 如果 event.preventDefault() 应该起作用,则 cancelable: true

本机事件的其他构造函数,如 MouseEventKeyboardEvent 等,接受特定于该事件类型的属性。例如,鼠标事件的 clientX

对于自定义事件,我们应该使用 CustomEvent 构造函数。它有一个名为 detail 的附加选项,我们应该将事件特定数据分配给它。然后,所有处理程序都可以通过 event.detail 访问它。

尽管有生成浏览器事件(如 clickkeydown)的技术可能性,但我们应该非常谨慎地使用它们。

我们不应该生成浏览器事件,因为这是一种运行处理程序的非常规方法。在大多数情况下,这是一种糟糕的架构。

本机事件可能会生成

  • 作为一种非常规方法,如果第三方库不提供其他交互方式,则可以使其按所需方式工作。
  • 对于自动化测试,在脚本中“单击按钮”并查看界面是否正确响应。

具有我们自己的名称的自定义事件通常出于架构目的而生成,以表示我们的菜单、滑块、旋转木马等内部发生的情况。

教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 – 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行代码 – 将其包装在 <pre> 标记中,对于 10 行以上的代码 – 使用沙盒 (plnkrjsbincodepen…)