原文链接: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 事件列表:
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);
清注意:通用的 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 -> 2
menu.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 -> nested
menu.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 手段使用。
对于自动化测试,需要在脚本中“单击按钮”,看看界面是否正确反应。
自定义事件通常作为架构需要而使用,去向开发者展示在我们的菜单、滑动条、轮播图等组件内部发生了什么。
自定义事件通常是出于架构目的而使用的。用于反映我们的菜单、滑块或者轮播图组件内部的运行状态。
(完)