JS - 自定义事件

587 阅读5分钟

所谓自定义事件,就是有别于浏览器的特定行为事件(如click, mousemove, keyup等)

自定义事件可以让我们自主命名事件名,并且可以通过特定的方法进行添加,触发以及删除

为了和浏览器内置事件进行区分,浏览器自定义事件又被称之为合成事件

在实际开发中,我们通常使用自定义事件来在组件和组件之间进行数据通信

我们可以使用EventCustomEvent构造函数来创建我们自己的自定义事件

区别在于 Event是所有JS内置事件的顶层父元素,所以一般使用Event来以脚本的方式触发一些浏览器内置事件

CustomEvent是专门用于创建一些用户自定义事件

基本使用

const btnEl = document.getElementById('btn')
btnEl.addEventListener('click', e => console.log('按钮被点击了'))

// 创建对应的事件对象
// 参数一: type - 事件名称 -- 自定义名称?
// 参数二: 配置对象
//    1. bubbles 布尔类型值 默认值为false
//       - 当值为true时表示该事件可以冒泡
//       - 当值为false时表示该事件不会冒泡
//
//    2. cancelable 布尔类型值 默认值为false
//       - 当值为true时,如果事件内部调用event.preventDefault方法
//       -    事件对应的dispatchEvent方法会返回false, 可以使用这点来模拟取消事件的默认行为
//       - 当值为false时,会忽略内部调用的event.preventDefault方法 ?
const event = new Event('click')

// 使用脚本方式触发事件
// elem.dispatchEvent(事件对象)
btnEl.dispatchEvent(event)
const btnEl = document.getElementById('btn')
btnEl.addEventListener('customEvent', e => console.log('按钮被点击了'))

// 虽然使用event可以绑定和触发自定义事件
// 但是从语义化角度,还是推荐使用CustomEvent来绑定和触发自定义事件
const event = new Event('customEvent')

btnEl.dispatchEvent(event)

配置对象

const btnEl = document.getElementById('btn')
const dvEl = document.getElementById('dv')

btnEl.addEventListener('customEvent', e => console.log('按钮被点击了'))
dvEl.addEventListener('customEvent', e => console.log('按钮点击事件发生了冒泡'))

const event = new Event('customEvent', {
  bubbles: true // 让自定义事件和内置事件一样,拥有相同的冒泡和捕获机制
})

btnEl.dispatchEvent(event)
/*
      =>
      按钮被点击了
      按钮点击事件发生了冒泡
    */
const btnEl = document.getElementById('btn')
const dvEl = document.getElementById('dv')

// 在JS中, 内置事件是可能拥有自己的默认行为的
// 但是用户自己创建的自定义事件是不可能存在对应的默认行为的
// 但是可以使用cancelable配置项来控制对应的自定义事件能否被取消
const event = new Event('customEvent', {
  cancelable: true // 允许事件被取消
})

btnEl.addEventListener('click', checkHide)

dvEl.addEventListener('customEvent', e => {
  if (confirm('是否取消事件')) {
    // 如果event开启了cancelable且调用了e.preventDefault()
    // 那么对应的dispatchEvent方法会返回false
    e.preventDefault()
  }
})

function checkHide() {
  // customEvent事件可能调用e.preventDefault()
  // 所以该方法的返回值可能为false也可能为true
  if (dvEl.dispatchEvent(event)) {
    dvEl.hidden = true
  } else {
    console.log('自定义事件被取消了')
  }
}

isTrusted

浏览器在触发事件的时候,会传入对应的事件对象,在事件对象中有一个boolean类型的属性isTrusted

当这个属性为true时,表示该事件是内置事件,反之就说明该事件是用户自定义事件

const btnEl = document.getElementById('btn')

btnEl.addEventListener('customEvent', e => console.log(e.isTrusted)) // => false
btnEl.addEventListener('click', e => console.log(e.isTrusted)) // => true

const event = new Event('customEvent')

btnEl.dispatchEvent(event)

具体事件类型

Event是所有内置事件的父类,我们在使用脚本触发对应内置事件的时候,可以使用具体的事件类来创建事件对象

例如: UIEvent MouseEvent PointEvent KeyboardEvent

当我们使用正确的构造器时候,就允许为该类型的事件指定标准属性

const mouseEvent = new MouseEvent("click", {
  bubbles: true,
  cancelable: true,
  // 因为该事件是MouseEvent类型,所以可以指定对应事件的标准属性
  clientX: 100,
  clientY: 100
})

const event = new Event("click", {
  bubbles: true,
  cancelable: true,
  // 如果指定了非对应事件类型的属性,对应属性会静默失效
  clientX: 100,
  clientY: 100
})

console.log(event.clientX) // => undefined
console.log(mouseEvent.clientX) // => 100

CustomEvent

对于我们自己定义的事件,应该使用CustomEvent构造器

CustomEvent构造器的使用和Event构造器的使用方式基本一致

唯一的区别是使用CustomEvent的时候,我们可以detail属性传递自定义属性

const btnEl = document.getElementById('btn')

const event = new CustomEvent('customEvent', {
  // CustomEvent除了有cancelable属性和bubbles属性外
  // 还存在一个属性detail 用于传递自定义数据
  // detail 属性可以是任何类型数据, 推荐使用对象类型,以便于传递多个自定义数据
  detail: {
    foo: '这是自定义数据'
  }
})

// 在按钮被点击的时候,同时触发按钮上的自定义事件
btnEl.addEventListener('click', () => btnEl.dispatchEvent(event))

// 通过detail传递的自定义数据,可以在事件对象的detail属性中获取
btnEl.addEventListener('customEvent', e => console.log(e.detail)) // => {foo: '这是自定义数据'}

事件中的事件是同步的

通常事件是在队列中处理的。也就是说:如果浏览器正在处理 onclick,这时发生了一个新的事件,例如鼠标移动了,那么它的处理程序会被排入队列,相应的 mousemove 处理程序将在 onclick 事件处理完成后被调用。

但是当一个事件是在另一个事件中发起的。例如使用 dispatchEvent。这类事件将会被立即处理,即在新的事件处理程序被调用之后,恢复到当前的事件处理程序

<button id="menu">Menu (click me)</button>

<script>
  menu.onclick = function() {
    alert(1);

    menu.dispatchEvent(new CustomEvent("menu-open", {
      bubbles: true
    }));

    alert(2);
  };

  // 输出顺序为:1 → nested → 2
  document.addEventListener('menu-open', () => alert('nested'));
</script>

如果我们希望事件中的事件以异步的方式进行执行,可以将事件中的事件包装到零延迟的 setTimeout 中。

即让事件中的事件转变为宏任务后单独加入事件队列中异步执行

<button id="menu">Menu (click me)</button>

<script>
  menu.onclick = function() {
    alert(1);

    setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
      bubbles: true
    })));

    alert(2);
  };

  // 输出顺序为:1 → 2 → nested
  document.addEventListener('menu-open', () => alert('nested'));
</script>