Web前端入门第 77 问:JavaScript 由程序触发绑定事件的几种方式

开发中经常会遇这样的需求:点击 A 元素的时候,需要触发 B 元素的事件,比如:点击一个 div 元素,然后触发 input:file 的 click 事件,用来选择文件上传。

click 方法

以上需求可通过元素的 click 方法触发:

html 复制代码
<style>
  .test2 {
    padding: 4px;
    margin-top: 12px;
  }
</style>
<input type="file" class="test1"><br>
<button class="test2">前端路引--事件测试</button>

<script>
  (() => {
    const test1 = document.querySelector('.test1')
    const test2 = document.querySelector('.test2')
    test2.addEventListener('click', () => {
      //  点击 test2 后,触发 test1 的 click 事件
      test1.click()
    })
  })()
</script>

效果:

点击事件有 click 方法可以使用,如果需求是点击 A 元素触发 B 元素的 mousedown 事件,那能用 mousedown 方法吗?

html 复制代码
<style>
  button {
    padding: 4px;
    margin-top: 12px;
  }
</style>

<button class="test1">前端路引--事件测试 test1</button><br>
<button class="test2">前端路引--事件测试 test2</button>

<script>
  (() => {
    const test1 = document.querySelector('.test1')
    const test2 = document.querySelector('.test2')
    test1.addEventListener('mousedown', () => {
      console.log('test1 mousedown')
    })
    test2.addEventListener('click', () => {
      //  点击 test2 后,触发 test1 的 click 事件
      test1.click()
      test1.mousedown();
    })
  })()
</script>

效果:

想法在实现上遇到了问题,html 元素上不存在 mousedown 方法,click 方法也只能触发 click 事件,无法触发绑定的 mousedown 事件,那么有办法可以做到吗?答案肯定是有的~~

dispatchEvent 方法

dispatchEvent 方法可以触发任意事件,其参数是一个实例化的 Event 对象。以上例子中使用 dispatchEvent 方法,实现效果如下:

html 复制代码
<style>
  button {
    padding: 4px;
    margin-top: 12px;
  }
</style>

<button class="test1">前端路引--事件测试 test1</button><br>
<button class="test2">前端路引--事件测试 test2</button>

<script>
  (() => {
    const test1 = document.querySelector('.test1')
    const test2 = document.querySelector('.test2')
    test1.addEventListener('mousedown', () => {
      console.log('test1 mousedown')
    })
    test2.addEventListener('click', () => {
      //  点击 test2 后,触发 test1 的 click 事件
      const event = new Event('mousedown')
      test1.dispatchEvent(event)
    })
  })()
</script>

效果:

Event 对象

语法:

js 复制代码
new Event(type, options)

该对象有两个参数:

第一个事件类型 type 为必传参数,不传则报错 Uncaught TypeError: Failed to construct 'Event': 1 argument required, but only 0 present.

第二个 options 参数为可选参数,拥有三个属性:
bubbles: 可选,Boolean 类型,默认值为 false,表示该事件是否冒泡。
cancelable: 可选,Boolean 类型,默认值为 false,表示该事件能否被取消。
composed: 可选,Boolean 类型,默认值为 false,指示事件是否会在影子 DOM 根节点之外触发侦听器。

bubbles 冒泡:

关于冒泡很好理解,就是子元素是否能冒泡到父元素,如下例子 test1 触发的事件将会冒泡到容器 test-container 上。

html 复制代码
<div class="test-container">
  <button class="test0">前端路引--事件测试 test0</button>
</div>
<button class="test1">前端路引--事件测试 test1</button>
<button class="test2">前端路引--事件测试 test2</button>

<script>
  (() => {
    const test0 = document.querySelector('.test0')
    const test1 = document.querySelector('.test1')
    const test2 = document.querySelector('.test2')
    document.querySelector('.test-container').addEventListener('mousedown', () => {
      console.log('test-container mousedown')
    })
    test1.addEventListener('click', () => {
      // 默认不允许冒泡
      const event = new Event('mousedown')
      test0.dispatchEvent(event)
    })
    test2.addEventListener('click', () => {
      // 配置 bubbles 允许冒泡
      const event = new Event('mousedown', { bubbles: true })
      test0.dispatchEvent(event)
    })
  })()
</script>

效果:

cancelable 事件能否取消:

true:表示事件是可取消的,调用 event.preventDefault() 会阻止浏览器的默认行为。
false:表示事件不可取消,调用 event.preventDefault() 无效。

实测就算传入的是传入为 false,也能调用 event.preventDefault() 并不会报错,对 Event 对象无效,但是对 MouseEvent 对象有用(参考后文)。

设置此参数可在事件的 event 参数上获取传入的值:

js 复制代码
<input type="file" name="" id="" class="file"><br>
<button class="test1">前端路引--事件测试 cancelable: false</button><br>
<button class="test2">前端路引--事件测试 cancelable: true</button>

<script>
  (() => {
    const file = document.querySelector('.file')
    const test1 = document.querySelector('.test1')
    const test2 = document.querySelector('.test2')
    file.addEventListener('click', () => {
      console.log('cancelable', event.cancelable);
    })
    test1.addEventListener('click', () => {
      const event = new Event('click', { cancelable: false })
      file.dispatchEvent(event)
    })
    test2.addEventListener('click', () => {
      const event = new Event('click', { cancelable: true })
      file.dispatchEvent(event)
    })
  })()
</script>

效果:

dispatchEvent 触发的 Event 对象,并 不会响应 元素本身的默认事件,比如 a 标签的跳转,input:file 的文件选择等。

composed 是否允许穿透 shadow DOM 节点:

如果不使用影子节点,这属性基本没啥用处,影子节点在常规开发中很少使用~~

一个简单示例:

html 复制代码
<div class="wrapper">
  <my-component id="host"></my-component>
</div>
<script>
  (() => {
    class MyComponent extends HTMLElement {
      constructor() {
        super();
        const shadow = this.attachShadow({ mode: 'open' });
        shadow.innerHTML = `
          <button id="inner-button1">影子节点内部按钮1--前端路引</button>
          <button id="inner-button2">影子节点内部按钮2--前端路引</button>
        `;
      }
    }
    customElements.define('my-component', MyComponent);

    const host = document.querySelector('#host');
    const innerBtn1 = host.shadowRoot.querySelector('#inner-button1')
    const innerBtn2 = host.shadowRoot.querySelector('#inner-button2')
    innerBtn1.addEventListener('click', () => {
      const event = new Event('custom-event', { 
        bubbles: true,    // 允许在 Shadow DOM 内部冒泡
        composed: false   // 禁止穿透到外部 DOM
      });
      innerBtn1.dispatchEvent(event);
    });
    innerBtn2.addEventListener('click', () => {
      const event = new Event('custom-event', { 
        bubbles: true,    // 允许在 Shadow DOM 内部冒泡
        composed: true   // 允许穿透到外部 DOM
      });
      innerBtn2.dispatchEvent(event);
    });
    // 尝试在外部 DOM 监听事件
    host.addEventListener('custom-event', () => {
      console.log('外部 DOM 监听到事件'); // 不会触发
    });
  })()
</script>

效果:

CustomEvent 对象

Event 对象没办法传入自定义数据,某些特定需求需要传入自定义参数时,可以祭出 CustomEvent 对象。

CustomEvent 继承 Event,所以 Event 支持的配置都支持,只是多了一个自定义数据字段。

示例:

html 复制代码
<button class="test1">前端路引--事件测试 test1</button><br>
<button class="test2">前端路引--事件测试 test2</button>

<script>
  (() => {
    const test1 = document.querySelector('.test1')
    const test2 = document.querySelector('.test2')
    test1.addEventListener('dev', (event) => {
      console.log('自定义数据:', event.detail);
    })
    test2.addEventListener('click', () => {
      //  点击 test2 后,触发 test1 的 click 事件
      const event = new CustomEvent('dev', {
        detail: {
          name: '前端路引',
          age: 1
        }
      })
      test1.dispatchEvent(event)
    })
  })()
</script>

以上代码使用了 CustomEvent 触发了自定义的 dev 事件,并传入了自定义数据。

效果:

MouseEvent

前面说了 dispatchEvent 触发的 Event 对象不会响应元素本身的默认事件,但可以通过 MouseEvent 对象来触发一些元素本身的默认事件。

如下例子:

html 复制代码
<input type="file" name="" id="" class="file"><br>
<button class="test1">前端路引--事件测试 cancelable: false</button><br>
<button class="test2">前端路引--事件测试 cancelable: true</button>

<script>
  (() => {
    const file = document.querySelector('.file')
    const test1 = document.querySelector('.test1')
    const test2 = document.querySelector('.test2')
    file.addEventListener('click', () => {
      event.preventDefault()
      console.log('cancelable', event.cancelable);
    })
    test1.addEventListener('click', () => {
      const event = new MouseEvent('click', { cancelable: false })
      const res = file.dispatchEvent(event)
      console.log(`${!res ? '调用了' : '没调用'} preventDefault`);
    })
    test2.addEventListener('click', () => {
      const event = new MouseEvent('click', { cancelable: true })
      const res = file.dispatchEvent(event)
      console.log(`${!res ? '调用了' : '没调用'} preventDefault`);
    })
  })()
</script>

效果:

dispatchEvent 返回值当 event 可被取消(cancelable 值为 true),且 event 中至少有一个事件处理程序调用了 Event.preventDefault() 方法时,返回 false。否则,返回 true。

写在最后

按照 MDN 的说法,由程序触发的事件,还有一个专用名词 合成事件,表示不是浏览器本身触发的事件。

除了本文例子中的几个 Event 对象外,还有一些其他对象,在使用时可参考 MDN 文档。

用户交互:

鼠标、键盘、触摸交互 MouseEvent, KeyboardEvent, TouchEvent

表单与输入:

输入框、表单提交 InputEvent, SubmitEvent

媒体控制:

音视频播放、设备流 MediaStreamTrackEvent

拖放与剪贴板:

拖拽操作、复制粘贴 DragEvent, ClipboardEvent

存储与通信:

本地存储、跨文档通信 StorageEvent, MessageEvent

错误与调试:

脚本错误捕获 ErrorEvent

设备与传感器:

方向、加速度检测 DeviceOrientationEvent

动画与过渡:

CSS 动画/过渡生命周期 AnimationEvent, TransitionEvent