JS 也可以生成事件:
- 不仅可以生成出于自身目的而创建的全新自定义事件
- 还可以生成内建事件(如
click
、mousedown
),这可能会有助于自动化测试
事件层次结构
类似于 DOM 元素类,事件类也形成一个 层次结构(hierarchy):
根类:Event
new Event(type: string, options?)
自定义事件:CustomEvent
new CustomEvent(type: string, options?)
- 自定义事件应该使用
CustomEvent
CustomEvent
和Event
一样,只有一点不同:options.detail?: any
:添加自定义数据- 实例可以通过
event.detail
只读属性获取自定义数据
- 自定义事件应该使用
从技术上讲,可以不用 detail
,因为可以在创建后将任何属性分配给常规的 new Event
对象中。但是 CustomEvent
提供了特殊的 detail
字段,以避免与其他事件属性的冲突
内建事件:XxxEvent
如果想要创建内建事件,应该使用具体的内建事件(如:new MouseEvent("click")
),而不是 new Event
正确的构造器允许为该类型的事件指定标准属性,如:
const event = new MouseEvent("click", {
bubbles: true,
cancelable: true,
clientX: 100,
clientY: 100
})
console.log(event.clientX) // 100
通用的 Event
构造器不允许这样做:
const event = new Event("click", {
bubbles: true, // 只有 bubbles 和 cancelable 可以工作
cancelable: true,
clientX: 100,
clientY: 100
})
console.log(event.clientX) // undefined,未知的属性被忽略了
从技术上讲,可以通过在创建后直接分配 event.clientX = 100
来解决这个问题。所以,这是一个方便和遵守规则的问题。浏览器生成的事件始终具有正确的类型
分派(触发)事件
EventTarget.dispatchEvent(event: Event)
event
:被派发的Event
,其event.target
为当前EventTarget
event.isTrusted
:区分“真实”用户事件和通过脚本生成的事件true
:来自真实用户操作的事件false
:脚本生成的事件
触发自定义事件
<h1 id="elem">Hello from the script!</h1>
<script>
// 监听事件
elem.addEventListener("hello", (e) => {
console.log(e.target) // elem
})
// 创建事件
const event = new Event("hello")
// 虽然 Event 可以正确工作,但是自定义事件还是推荐 CustomEvent
// 分派该事件
elem.dispatchEvent(event)
</script>
冒泡示例:
<h1 id="elem">Hello from the script!</h1>
<script>
// 在 document 上监听
document.addEventListener("hello", function(event) {
console.log(event.detail.a); // test
})
// 在 elem 上 dispatch
const event = new CustomEvent("hello", {
bubbles: true, // 允许冒泡
detail: { a: 'test' } // 自定义数据
})
elem.dispatchEvent(event);
</script>
注意:自定义事件应该使用 addEventListener
,因为 on<event>
仅存在于内建事件中,document.onhello
则无法运行
触发内建事件
<button id="elem" onclick="alert('Click!');">Autoclick</button>
<script>
const event = new Event("click")
// 虽然 Event 可以正确工作,但是内建事件还是推荐 MouseEvent
elem.dispatchEvent(event)
</script>
尽管技术上可以生成和触发浏览器内建事件,但还是应谨慎使用它们
因为这是运行处理程序的一种怪异(hacky)方式。大多数时候,这都是糟糕的架构
可以生成原生事件的场景:
- 对于自动化测试
- 如果第三方程序库不提供其他交互方式,那么这是使第三方程序库工作所需的一种肮脏手段
事件处理是同步执行的
通常事件是在队列中处理的。也就是说:如果浏览器正在处理 onclick
,这时发生了一个新的事件,例如鼠标移动了,那么它的处理程序会被排入队列,相应的 mousemove
处理程序将在 onclick
事件处理完成后被调用
值得注意的例外情况就是,一个事件是在另一个事件中发起的(如:使用 dispatchEvent
、调用触发其他事件的方法),这类事件将会被立即处理,即在新的事件处理程序被调用之后,恢复到当前的事件处理程序
例如:
<button id="menu">Menu (click me)</button>
<script>
menu.onclick = function() {
console.log(1)
menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
}))
console.log(2)
};
document.addEventListener('menu-open', () => console.log('nested'))
</script>
结果是:1 → nested → 2
如果想要当前事件处理函数不被打断,可以将其放到下一个 事件循环 中触发:
<button id="menu">Menu (click me)</button>
<script>
menu.onclick = function() {
console.log(1)
// 放到下一个事件循环中触发事件
setimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
})))
console.log(2)
};
document.addEventListener('menu-open', () => console.log('nested'))
</script>
结果是:1 → 2 → nested