原文链接:http://javascript.info/dispatch-events,translate with ❤️ by zhangbao.
我们不仅可以绑定事件处理程序,也可以使用 JavaScript 生成事件。
自定义事件可用于创建“图形组件”。例如,菜单的根元素可以触发事件,告诉开发者菜单发生了什么:open(菜单打开),select(选择菜单项)等等。
我们也可以生成一些内置事件,比如 click、mousedown 等,这有助于测试。
事件构造器
事件也是有继承关系的,就像 DOM 元素类一样。根是内置的 Event 类。
我们像下面这样创建 Event 对象:
let event = new Event(type[, options]);
参数:
type:事件类型,是一个字符串,可以是”click“,也可以是我们自定义的诸如”hey-ho!“的事件名。options:选项对象,包含两个可选属性:bubbles: true/false:true表示事件是冒泡的。cancelable: true/false:true表示事件“默认行为”可被阻止。之后我们会学到怎样在自定义事件中使用它。
两个属性值默认都为 false:{ bubbles: false, cancelable: false }。
dispatchEvent
创建完事件对象后,我们就能使用 elem.dispatchEvent(event) 在元素上“执行”它。
事件处理器会像对待普通的内置事件一样做出响应。如果创建事件时添加了 bubbles 标识,就表示事件是冒泡的。
下面例子中的事件是通过 JavaScript 触发的,发现按钮就像被点击了一样,执行了 onclick 特性中的代码逻辑。
<button id="elem" onclick="alert('被点击了!')">自动点击</button><script>let event = new Event('click');elem.dispatchEvent(event);</script>
event.isTrusted
有一种方法,可以区分一个事件是由用户产生或脚本生成的。
event.isTrusted属性值为true时,就表示事件是由用户生成的;如果为false就表示事件是由脚本生成的。
冒泡的例子
我们可以创建一个冒泡事件 hello,然后使用 document 对象捕获它。
我们需要做的就是把 bubbles 的属性值设为 true。
<h1 id="elem">来自脚本的问候!</h1><script>document.addEventListener('hello', function (event) {alert('来自' + event.target.tagName + '的问候');});let event = new Event('hello', { bubbles: true });elem.dispatchEvent(event);</script>
注意:
我们应该使用
addEventListener绑定自定义事件,因为on<事件名>的绑定方式只对内置事件有效,用document.onhello则是无效的。必须设置
bubbles: true,否则事件不会冒泡。
内置事件和自定义事件在表现上是一致的,也分捕获和冒泡阶段。
MouseEvent,KeyboardEvent 和其他
这里列举了从《UI 事件规范》中描述的 UI 事件列表:
UIEventFocusEventMouseEventWheelEventKeyBoardEvent……
我们应该选择使用上面具体的事件类型构造器,来创建事件实例,而不是简单暴力地一概使用 new Event 去创建事件对象。例如:new MouseEvent('click')。
使用特定类型的事件构造器时,可以允许我们指定,仅存在于该事件中的特定初始化参数。
像鼠标事件的 clientX/clientY:
let event = new MouseEvent('click', {bubbles: true,cancelable: true,clientX: 100,clientY: 100});alert(event.clientX);
清注意:通用的 Event 构造函数就不包含这些选项。
我们试试:
let event = new Event('click', {bubbles: true,cancelable: true,clientX: 100,clientY: 100})alert(event.clientX); // undefined(不支持的属性被忽略了)
从技术上讲,我们可以在创建后通过直接指定 event.clientX = 100 的方式来解决这个问题。所以这是便利和遵守规则之间的博弈。而浏览器生成的事件始终具有正确的类型(即总是使用正确的事件构造器生成事件)。
完整的不同 UI 事件的属性列表在规范中有详细列举,比如 MouseEvent 对象的。
自定义事件
对于像我们创建的自定义 “hello” 事件,我们应该使用 new CustomEvent() 初始化更合适些。技术上讲,CustomEvent 和 Event 几乎是一样的,只有一点区别。
CustomEvent 构造器,还给我们提供了可选的第二个(对象类型)参数。我们可以将想要随事件传递的详细信息赋值给 detail 属性。
例如:
<h1 id="elem">Hello, eveybody.</h1><script>// 自定义事件附加的详细信息会出现在处理程序中elem.addEventListener('hello', function (event) {alert(event.detail.name);});elem.dispatchEvent(new CustomEvent('hello', {detail: { name: 'zhangsan' }}));</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() 阻止默认行为,兔子就不会隐藏了:
<pre id="rabbit">|\ /|\|_|//. .\=\_Y_/={>o<}</pre><script>// hide() 会在 2 秒后自动被调用function hide() {let event = new CustomEvent('hide', {cancelable: true // 没有设置这个标记的话,就无法调用 preventDefault() 取消默认行为});// dispatchEvent 调用后是有返回值的,我们就根据返回值判断要不要隐藏兔子if (!rabbit.dispatchEvent(event)) {alert('默认行为在控制器里被阻止了。');} else {rabbit.hide = true;}rabbit.addEventListener('hide', function (event) {if (confirm('调用 preventDefault?')) {event.preventDefault();}});}// 2 秒后隐藏setTimeout(hide, 2000);</script>
有嵌套事件的情况下代码是同步执行的
通常事件处理是异步的。也就是:如果浏览器正在处理 onclick 事件时,又来了一个新事件,那么它不会等到 onclick 事件处理完毕后才执行。
但有一个例外情况,就是在一个事件中触发另一个事件。
这种情况下,控制权会跳跃到嵌套的内部事件控制程序中,待其处理完毕后,控制权转交给外部的事件控制程序。
例如:有一个 menu-open 事件,在 onclick 事件内部被同步执行。
<button id="menu">菜单(点击我)</button><script>// 1 -> nested -> 2menu.onclick = function () {alert(1);// 接着执行事件程序器中的 alert('nested')menu.dispatchEvent(new CustomEvent('menu-open', {bubbles: true}));alert(2);};document.addEventListener('menu-open', () => alert('nested'));</script>
请注意,发生下 #menu 上的事件 menu-open 会冒泡到 document。当这个嵌套事件处理完毕后,执行主动权交给了外部的处理程序(onclick),继续执行下面的代码。
这不仅仅是关于 dispatchEvent,还有其他案例。 事件处理程序中的 JavaScript 可以调用导致其他事件的方法——它们也是同步处理的。
如果我们不喜欢它,我们可以在 onclick 结束时再写 dispatchEvent(或其他触发事件的调用);如果不方便,也可以将代码包装在 setTimeout(..., 0) 中:
<button id="menu">菜单(点击我)</button><script>// 1 -> 2 -> nestedmenu.onclick = function () {alert(1);setTimeout(() => {menu.dispatchEvent(new CustomEvent('menu-open'), {bubbles: true});}, 0);alert(2;)};document.addEventListener('menu-open', () => alert('nested'));</script>
总结
为了生成一个事件,我们首先要创建一个事件对象。
通用的事件构造器 Event(name, options) 接收一个事件名和一个选项对象,选项对象中包含两个可用属性:
bubbles: true/false:事件是否冒泡。cancelable: true/false:事件的处理程序中可否调用event.preventDefault()。
像其他具体的 MouseEvent、KeyBoardEvent 等这些原生事件构造函数,还支持接收特定于其类型的事件属性完成初始化。例如,鼠标事件的 clientX 属性。
对于自定义事件,我们应该使用构造器 CustomEvent ,因为它还额外提供了 detail 选项,用来提供事件的详情信息,在所有的处理程序中都可以通过 event.detail 拿到它。
虽然技术上能够生成诸如 click 或者 keydown 事件,不过我们还是要小心使用。
我们不应该生成浏览器事件,因为它是运行处理程序的一种 hacky 方式,大部分时间这都一种糟糕的架构方式。
需要手动生成原生事件的场景:
当我们使用的第三方库,没有提供给我们需要的方法去交互时,作为一种不太好的 hack 手段使用。
对于自动化测试,需要在脚本中“单击按钮”,看看界面是否正确反应。
自定义事件通常作为架构需要而使用,去向开发者展示在我们的菜单、滑动条、轮播图等组件内部发生了什么。
自定义事件通常是出于架构目的而使用的。用于反映我们的菜单、滑块或者轮播图组件内部的运行状态。
(完)
event.isTrusted