原文链接:http://javascript.info/dispatch-events,translate with ❤️ by zhangbao.

我们不仅可以绑定事件处理程序,也可以使用 JavaScript 生成事件。

自定义事件可用于创建“图形组件”。例如,菜单的根元素可以触发事件,告诉开发者菜单发生了什么:open(菜单打开),select(选择菜单项)等等。

我们也可以生成一些内置事件,比如 clickmousedown 等,这有助于测试。

事件构造器

事件也是有继承关系的,就像 DOM 元素类一样。根是内置的 Event 类。

我们像下面这样创建 Event 对象:

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

参数:

  • type:事件类型,是一个字符串,可以是”click“,也可以是我们自定义的诸如”hey-ho!“的事件名。

  • options:选项对象,包含两个可选属性:

    • bubbles: true/falsetrue 表示事件是冒泡的。

    • cancelable: true/falsetrue 表示事件“默认行为”可被阻止。之后我们会学到怎样在自定义事件中使用它。

两个属性值默认都为 false{ bubbles: false, cancelable: false }

dispatchEvent

创建完事件对象后,我们就能使用 elem.dispatchEvent(event) 在元素上“执行”它。

事件处理器会像对待普通的内置事件一样做出响应。如果创建事件时添加了 bubbles 标识,就表示事件是冒泡的。

下面例子中的事件是通过 JavaScript 触发的,发现按钮就像被点击了一样,执行了 onclick 特性中的代码逻辑。

  1. <button id="elem" onclick="alert('被点击了!')">自动点击</button>
  2. <script>
  3. let event = new Event('click');
  4. elem.dispatchEvent(event);
  5. </script>

派发自定义事件 - 图1event.isTrusted

有一种方法,可以区分一个事件是由用户产生或脚本生成的。

event.isTrusted 属性值为 true 时,就表示事件是由用户生成的;如果为 false 就表示事件是由脚本生成的。

冒泡的例子

我们可以创建一个冒泡事件 hello,然后使用 document 对象捕获它。

我们需要做的就是把 bubbles 的属性值设为 true

  1. <h1 id="elem">来自脚本的问候!</h1>
  2. <script>
  3. document.addEventListener('hello', function (event) {
  4. alert('来自' + event.target.tagName + '的问候');
  5. });
  6. let event = new Event('hello', { bubbles: true });
  7. elem.dispatchEvent(event);
  8. </script>

注意:

  1. 我们应该使用 addEventListener 绑定自定义事件,因为 on<事件名> 的绑定方式只对内置事件有效,用 document.onhello 则是无效的。

  2. 必须设置 bubbles: true,否则事件不会冒泡。

内置事件和自定义事件在表现上是一致的,也分捕获和冒泡阶段。

MouseEvent,KeyboardEvent 和其他

这里列举了从《UI 事件规范》中描述的 UI 事件列表:

  • UIEvent

  • FocusEvent

  • MouseEvent

  • WheelEvent

  • KeyBoardEvent

  • ……

我们应该选择使用上面具体的事件类型构造器,来创建事件实例,而不是简单暴力地一概使用 new Event 去创建事件对象。例如:new MouseEvent('click')

使用特定类型的事件构造器时,可以允许我们指定,仅存在于该事件中的特定初始化参数。

像鼠标事件的 clientX/clientY

  1. let event = new MouseEvent('click', {
  2. bubbles: true,
  3. cancelable: true,
  4. clientX: 100,
  5. clientY: 100
  6. });
  7. alert(event.clientX);

清注意:通用的 Event 构造函数就不包含这些选项。

我们试试:

  1. let event = new Event('click', {
  2. bubbles: true,
  3. cancelable: true,
  4. clientX: 100,
  5. clientY: 100
  6. })
  7. alert(event.clientX); // undefined(不支持的属性被忽略了)

从技术上讲,我们可以在创建后通过直接指定 event.clientX = 100 的方式来解决这个问题。所以这是便利和遵守规则之间的博弈。而浏览器生成的事件始终具有正确的类型(即总是使用正确的事件构造器生成事件)。

完整的不同 UI 事件的属性列表在规范中有详细列举,比如 MouseEvent 对象的。

自定义事件

对于像我们创建的自定义 “hello” 事件,我们应该使用 new CustomEvent() 初始化更合适些。技术上讲,CustomEventEvent 几乎是一样的,只有一点区别。

CustomEvent 构造器,还给我们提供了可选的第二个(对象类型)参数。我们可以将想要随事件传递的详细信息赋值给 detail 属性。

例如:

  1. <h1 id="elem">Hello, eveybody.</h1>
  2. <script>
  3. // 自定义事件附加的详细信息会出现在处理程序中
  4. elem.addEventListener('hello', function (event) {
  5. alert(event.detail.name);
  6. });
  7. elem.dispatchEvent(new CustomEvent('hello', {
  8. detail: { name: 'zhangsan' }
  9. }));
  10. </script>

detail 属性值可以是任何类型数据。技术上讲,我们也可以没必要使用它,因为我们完全可以在初始化 new Event 对象后,再通过额外属性赋值的方式达到效果。

event.preventDefault()

如果指定了 cancelable: true 标识,那么脚本生成的事件,就可以调用 event.preventDefault()

当然,如果不是标准事件,浏览器是一无所知的,不存在“默认浏览器行为”这个说法。

但是生成的事件代码可能会在 dispatchEvent 之后,执行一些操作(注意,dispatchEvent 调用之后,是有返回值的——当事件处理函数内部调用了 .preventDefault() 的话,就返回 false,否则返回 true)。

在处理程序中调用 event.preventDefault() 代表一种信号,表示这些操作不应该被执行。

比如下面例子里的 hide() 函数。在元素 #rabbit 上触发 hide 事件,告诉页面其他的部分,这个兔子要隐藏了。

通过 rabbit.addEventListener('hide', ...) 绑定的事件处理器会执行。如果需要,可以在处理器中调用 event.preventDefault() 阻止默认行为,兔子就不会隐藏了:

  1. <pre id="rabbit">
  2. |\ /|
  3. \|_|/
  4. /. .\
  5. =\_Y_/=
  6. {>o<}
  7. </pre>
  8. <script>
  9. // hide() 会在 2 秒后自动被调用
  10. function hide() {
  11. let event = new CustomEvent('hide', {
  12. cancelable: true // 没有设置这个标记的话,就无法调用 preventDefault() 取消默认行为
  13. });
  14. // dispatchEvent 调用后是有返回值的,我们就根据返回值判断要不要隐藏兔子
  15. if (!rabbit.dispatchEvent(event)) {
  16. alert('默认行为在控制器里被阻止了。');
  17. } else {
  18. rabbit.hide = true;
  19. }
  20. rabbit.addEventListener('hide', function (event) {
  21. if (confirm('调用 preventDefault?')) {
  22. event.preventDefault();
  23. }
  24. });
  25. }
  26. // 2 秒后隐藏
  27. setTimeout(hide, 2000);
  28. </script>

有嵌套事件的情况下代码是同步执行的

通常事件处理是异步的。也就是:如果浏览器正在处理 onclick 事件时,又来了一个新事件,那么它不会等到 onclick 事件处理完毕后才执行。

但有一个例外情况,就是在一个事件中触发另一个事件。

这种情况下,控制权会跳跃到嵌套的内部事件控制程序中,待其处理完毕后,控制权转交给外部的事件控制程序。

例如:有一个 menu-open 事件,在 onclick 事件内部被同步执行。

  1. <button id="menu">菜单(点击我)</button>
  2. <script>
  3. // 1 -> nested -> 2
  4. menu.onclick = function () {
  5. alert(1);
  6. // 接着执行事件程序器中的 alert('nested')
  7. menu.dispatchEvent(new CustomEvent('menu-open', {
  8. bubbles: true
  9. }));
  10. alert(2);
  11. };
  12. document.addEventListener('menu-open', () => alert('nested'));
  13. </script>

请注意,发生下 #menu 上的事件 menu-open 会冒泡到 document。当这个嵌套事件处理完毕后,执行主动权交给了外部的处理程序(onclick),继续执行下面的代码。

这不仅仅是关于 dispatchEvent,还有其他案例。 事件处理程序中的 JavaScript 可以调用导致其他事件的方法——它们也是同步处理的。

如果我们不喜欢它,我们可以在 onclick 结束时再写 dispatchEvent(或其他触发事件的调用);如果不方便,也可以将代码包装在 setTimeout(..., 0) 中:

  1. <button id="menu">菜单(点击我)</button>
  2. <script>
  3. // 1 -> 2 -> nested
  4. menu.onclick = function () {
  5. alert(1);
  6. setTimeout(() => {
  7. menu.dispatchEvent(new CustomEvent('menu-open'), {
  8. bubbles: true
  9. });
  10. }, 0);
  11. alert(2;)
  12. };
  13. document.addEventListener('menu-open', () => alert('nested'));
  14. </script>

总结

为了生成一个事件,我们首先要创建一个事件对象。

通用的事件构造器 Event(name, options) 接收一个事件名和一个选项对象,选项对象中包含两个可用属性:

  • bubbles: true/false:事件是否冒泡。

  • cancelable: true/false:事件的处理程序中可否调用 event.preventDefault()

像其他具体的 MouseEventKeyBoardEvent 等这些原生事件构造函数,还支持接收特定于其类型的事件属性完成初始化。例如,鼠标事件的 clientX 属性。

对于自定义事件,我们应该使用构造器 CustomEvent ,因为它还额外提供了 detail 选项,用来提供事件的详情信息,在所有的处理程序中都可以通过 event.detail 拿到它。

虽然技术上能够生成诸如 click 或者 keydown 事件,不过我们还是要小心使用。

我们不应该生成浏览器事件,因为它是运行处理程序的一种 hacky 方式,大部分时间这都一种糟糕的架构方式。

需要手动生成原生事件的场景:

  • 当我们使用的第三方库,没有提供给我们需要的方法去交互时,作为一种不太好的 hack 手段使用。

  • 对于自动化测试,需要在脚本中“单击按钮”,看看界面是否正确反应。

自定义事件通常作为架构需要而使用,去向开发者展示在我们的菜单、滑动条、轮播图等组件内部发生了什么。

自定义事件通常是出于架构目的而使用的。用于反映我们的菜单、滑块或者轮播图组件内部的运行状态。

(完)