JS 也可以生成事件:

  • 不仅可以生成出于自身目的而创建的全新自定义事件
  • 还可以生成内建事件(如 clickmousedown),这可能会有助于自动化测试

事件层次结构

类似于 DOM 元素类,事件类也形成一个 层次结构(hierarchy):

根类:Event

  • new Event(type: string, options?)
    • type:事件名称
    • options:对象
      • bubbles?: boolean:默认 false,表示该事件是否冒泡,事件传播流程
      • cancelable?: boolean:默认 false,表示该事件是否可被取消,浏览器默认行为
      • composed?: boolean:默认 false,指示事件是否会在影子 DOM 根节点之外触发侦听器

自定义事件:CustomEvent

  • new CustomEvent(type: string, options?)
    • 自定义事件应该使用 CustomEvent
    • CustomEventEvent 一样,只有一点不同:
      • 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