JavaScript DOM 事件详解

180 阅读13分钟

1. DOM事件级别

  • DOM级别一共可以分为四个级别:DOM0级、DOM1级、DOM2级和DOM3级。
  • DOM事件分为3个级别:DOM0级事件处理,DOM2级事件处理和DOM3级事件处理。由于DOM 1级中没有事件的相关内容,所以没有DOM 1级事件。

DOM0级事件

DOM0级处理事件就是将一个函数赋值给一个事件处理属性。

<button id="btn">OK</button>
<script>
    const btn = document.getElementById('btn')
    btn.onclick = function() {
        console.log('OK')
    }
</script>

删除事件处理程序

btn.onclick = null

DOM0级事件处理程序的缺点在于一个处理程序无法同时绑定多个处理函数,比如再给上面中的按钮再绑定多一个处理函数是不行的,因为前面的会被覆盖。

DOM2级事件

DOM2级事件在DOM0级事件的基础上弥补了一个处理程序无法同时绑定多个处理函数的缺点,允许给一个处理程序添加多个处理函数。

DOM2级事件定义了addEventListenerremoveEventListener两个方法,分别用来绑定和解绑事件。 el.addEventListener(event-name, callback, useCapture)
el.removeEventListener(event-name, callback, useCapture)

  • event-name: 事件名称,可以是标准的DOM事件
  • callback: 回调函数
  • useCapture: 默认是false,表示在冒泡阶段调用事件处理程序;true:表示在捕获阶段调用事件处理程序 第三个参数除了布尔值useCapture,还可以是一个监听器配置对象,定制事件监听行为。该对象有以下属性:
    • capture:布尔值,如果设为true,表示监听函数在捕获阶段触发,默认为false,在冒泡阶段触发。
    • once:布尔值,如果设为true,表示监听函数执行一次就会自动移除,后面将不再监听该事件。该属性默认值为false
    • passive:布尔值,设为true时,表示禁止监听函数调用preventDefault()方法。如果调用了,浏览器将忽略这个要求,并在控制台输出一条警告。该属性默认值为false
    • signal:该属性的值为一个 AbortSignal 对象,为监听器设置了一个信号通道,用来在需要时发出信号,移除监听函数。
<script>
  const btn = document.getElementById('btn')
  const fn1 = () => {
    console.log('OK1')
  }
  const fn2 = () => {
    console.log('OK2')
  }
  btn.addEventListener('click', fn1)
  btn.addEventListener('click', fn2)
</script>

移除事件绑定

btn.removeEventListener('click', fn1)

事件对象 在触发DOM上的某个事件时,会产生一个事件对象event,这个对象中包含着所有与事件有关的信息。event对象包含与创建它的特定事件有关的属性和方法。触发事件的类型不一样,可用的属性和方法也不一样。下面列出了所有事件都有的成员。

  • bubbles: 表明事件是否冒泡。Boolean类型
  • cancelable: 表明是否可以取消事件的默认行为。Boolean类型
  • currentTarget:其事件处理程序当前正在处理的那个元素。Element类型
  • defaultPrevented: 为true表示已调用了preventDefault()(DOM3级事件中新增)。Boolean类型
  • detail:与事件相关的细节信息。Integer类型
  • eventPhase:调用事件处理程序的阶段:1表示捕获阶段,2表示“处于目标”,3表示冒泡阶段。Integer类型
  • preventDefault: 取消事件的默认行为。如果cancelabletrue,则可以调用这个方法。Function类型
  • stopImmediatePropagation:取消事件的进一步捕获和冒泡,同时阻止任何事件处理程序被调用(DOM3级事件新增)。Function类型
  • stopPropagation:取消事件的进一步捕获和冒泡。如果bubblestrue,则可以调用这个方法。Function类型
  • target: 事件目标
  • trusted: 为true表示事件是浏览器生成的。为false表示事件是由开发人员通过JavaScript创建的(DOM3级事件中新增)。Boolean类型
  • type: 被触发的事件的类型。String类型
  • view:与事件关联的抽象视图。等同于发生事件的window对象。AbstractView类型

DOM3级事件

DOM3级事件在DOM2级事件的基础上添加了更多的事件类型,全部类型如下:

  • UI事件,当用户与页面上的元素交互时触发,如:loadunloadselectresizescroll
  • 焦点事件,当元素获得或失去焦点时触发,如:blurfocus
  • 鼠标事件,当用户通过鼠标在页面执行操作时触发,如:clickdbclickmouseup
  • 滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel
  • 文本事件,当在文档中输入文本时触发,如:textInput
  • 键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydownkeypresskeyup
  • 合成事件,当为IME(输入法编辑器)输入字符时触发,如:compositionstart
  • 变动事件,当底层DOM结构发生变化时触发,如:DOMsubtreeModified

同时DOM3级事件也允许使用者自定义一些事件。

2. 事件流

事件流描述的是从页面中接收事件的顺序。

“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。

图片.png

我们看下冒泡的例子:

<div id="myDiv">点击我</div>
<script>
  const myDiv = document.getElementById('myDiv')
  document.addEventListener('click', () => {
    console.log('document-click')
  })
  document.documentElement.addEventListener('click', () => {
    console.log('html-click')
  })
  document.body.addEventListener('click', () => {
    console.log('body-click')
  })
  myDiv.addEventListener('click', () => {
    console.log('myDiv-click')
  })
</script>

结果如下:
图片.png

改为捕获:

<div id="myDiv">点击我</div>
<script>
  const myDiv = document.getElementById('myDiv')
  document.addEventListener('click', () => {
    console.log('document-click')
  }, true)
  document.documentElement.addEventListener('click', () => {
    console.log('html-click')
  }, true)
  document.body.addEventListener('click', () => {
    console.log('body-click')
  }, true)
  myDiv.addEventListener('click', () => {
    console.log('myDiv-click')
  }, true)
</script>

结果如下:
图片.png

3. 事件委托

对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。比如,click事件会一直冒泡的document层次。也就是说,我们可以为整个页面指定一个onclick事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。

比如下面的例子:

<ul id="list">
  <li id="item1">A</li>
  <li id="item2">B</li>
  <li id="item3">C</li>
</ul>

如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。借助事件代理,我们只需要给父容器ul绑定方法即可,这样不管点击的是哪一个后代元素,都会根据冒泡传播的传递机制,把容器的click行为触发,然后把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而完成不同的事。

const list = document.getElementById('list')
list.addEventListener('click', e => {
    const target = e.target
    switch (target.id) {
      case 'item1':
        console.log('item1-click')
        break
      case 'item2':
        console.log('item2-click')
        break
      case 'item3':
        console.log('item3-click')
        break
    }
})

4. 事件类型

4.1 鼠标事件

  • click:在用户单击主鼠标按钮(一般是左边的按钮)或者按下回车键时触发。
  • dblclick:在用户双击击主鼠标按钮(一般是左边的按钮)时触发。
  • mousedown:在用户按下了任意鼠标按钮时触发。不能通过键盘触发这个事件。
  • mouseup:在用户释放鼠标按钮时触发。不能通过键盘触发这个事件。
  • mouseenter:在鼠标光标从元素外部首次移动到元素范围之内时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发
  • mouseleave:在位于元素上方的鼠标光标移动到元素范围之外时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发
  • mousemove:当鼠标指针在元素内部移动时重复的触发。不能通过键盘触发这个事件。
  • mouseout:在鼠标指针位于已元素上方,然后用户将其移入另一个元素时触发。又移入得了另一个元素可能位于前一个元素的外部,也可能是这个元素的子元素。不能通过键盘触发这个事件。
  • mouseover:在鼠标指针位于一个元素外部,然后用户将其首次移入另一个元素边界之内时触发。不能通过键盘触发这个事件。

只有在同一个元素上相继触发mousedownmouseup事件,才会触发click事件。 一个dblclick事件被触发,会经历以下事件:

  1. mousedown
  2. mouseup
  3. click
  4. mousedown
  5. mouseup
  6. click
  7. dblclick
clientX、clientY

clientXclientY属性 表示点击位置距离当前body可视区域的x,y坐标

图片.png

const box = document.getElementById('box')
box.addEventListener('click', e => {
    console.log('clientX', e.clientX)
    console.log('clientY', e.clientY)
})
pageX、pageY

pageXpageY属性 是从页面本身而非视口的左边和顶边计算的。如果页面有滚动,pageXpageY是包括被卷去的body部分的长度。

图片.png

screenX、screenY

screenXscreenY属性,是相对于整个电脑屏幕的位置。

图片.png

修改键

虽然鼠标事件主要是使用鼠标来触发的,但在按下鼠标时键盘上某些键的状态也可以影响到所要采取的操作。这些修改键就是Shift、Ctrl、Alt、Meta(Windows键/Cmd键),对应的属性分别是shiftKeyctrlKeyaltKeymetaKey,这些属性都是布尔值,对应键按下就是true,否则为false
我们来看下面的例子。

const box = document.getElementById('box')
box.addEventListener('click', e => {
    const keys = []
    if (e.shiftKey) {
      keys.push('shift')
    }
    if (e.ctrlKey) {
      keys.push('ctrl')
    }
    if (e.altKey) {
      keys.push('alt')
    }
    if (e.metaKey) {
      keys.push('meta')
    }
    console.log('Keys:', keys.join('+'))
})
按钮事件

对于mousedownmouseup事件来说,在其event对象存在一个button属性:
0: 主鼠标按钮,常规设置就是鼠标左键
1:中间鼠标按钮,即鼠标滚轮按钮
2:次鼠标按钮,常规设置就是鼠标右键

4.2 键盘事件

  • keydown: 当用户按下键盘上任意键时触发,而且如果按住不放的话,会重复触发此事件。
  • keypress:当用户按下键盘上的字符键时触发,而且如果按住不放的话,会重复触发此事件。Alt , Ctrl ,Shift 等不会触发
  • keyup: 当用户 释放键盘上的键时触发。

键盘事件与鼠标事件一样,都支持相同的修改键

key属性

event对象有key属性(注意keyCode属性已经被弃用,这里不再细说)。在按下某个字符时,key的值就是相对应的文本字符串(如“k”或“M”);在按下非字符键时,key的值是相应键的名(如“Shift”或“Down”).

4.3 复合事件

  • compositionstart:在IME(Input Method Editor 输入法编辑器)的文本复合系统打开时触发,表示要开始输入了。
  • compositionupdate:在向输入字段中插入新字符时触发。
  • compositionend:在IME的文本复合系统关闭时触发,表示返回正常键盘输入状态。

4.4 HTML5事件

contextmenu事件

在元素中用户右击鼠标时触发并打开上下文菜单。

<div id="myDiv">右键点击显示菜单</div>
<ul id="menu" style="visibility: hidden; position: absolute">
  <li>菜单1</li>
  <li>菜单2</li>
  <li>菜单3</li>
</ul>
<script>
  const myDiv = document.getElementById('myDiv')
  const menu = document.getElementById('menu')
  myDiv.addEventListener('contextmenu', ev => {
    // 首先取消默认行为
    ev.preventDefault()
    menu.style.visibility = 'visible'
    menu.style.left = ev.clientX + 'px'
    menu.style.top = ev.clientY + 'px'
  })
  document.addEventListener('click', () => {
    menu.style.visibility = 'hidden'
  })
</script>

beforeunload 事件

beforeunload 事件会在window 上触发,用意是给开发者提供阻止页面被卸载的机会。这个事件 会在页面即将从浏览器中卸载时触发,如果页面需要继续使用,则可以不被卸载。

window.addEventListener('beforeunload', (ev) => {
  // 根据规范,需要先取消默认事件
  ev.preventDefault()
  // Chrome 需要设置returnValue
  ev.returnValue = ''
});

4.5 触摸事件

  • touchstart:手指放到屏幕上时触发(即使有一个手指已经放在了屏幕上)。
  • touchmove:手指在屏幕上滑动时连续触发。在这个事件中调用preventDefault()可以阻止 滚动。
  • touchend:手指从屏幕上移开时触发。
  • touchcancel:系统停止跟踪触摸时触发。文档中并未明确什么情况下停止跟踪。

以上事件都会冒泡,也都可以被取消。尽管触摸事件不属于 DOM 规范,但浏览器仍然以兼容 DOM 的方式实现了它们。因此,每个触摸事件的event对象都提供了鼠标事件的公共属性:bubblescancelableviewclientXclientYscreenXscreenYdetailaltKeyshiftKeyctrlKeymetaKey

除了这些公共的 DOM 属性,触摸事件还提供了以下 3 个属性用于跟踪触点。

  • touches:Touch 对象的数组,表示当前屏幕上的每个触点。
  • targetTouches:Touch 对象的数组,表示特定于事件目标的触点。
  • changedTouches:Touch 对象的数组,表示自上次用户动作之后变化的触点。

每个 Touch 对象都包含下列属性。

  • clientX:触点在视口中的 x 坐标。
  • clientY:触点在视口中的 y 坐标。
  • identifier:触点 ID。
  • pageX:触点在页面上的 x 坐标。
  • pageY:触点在页面上的 y 坐标。
  • screenX:触点在屏幕上的 x 坐标。
  • screenY:触点在屏幕上的 y 坐标。
  • target:触摸事件的事件目标。

下面是一个简单的例子:

function load() {
    document.addEventListener('touchstart', touch, false)
    document.addEventListener('touchmove', touch, false)
    document.addEventListener('touchend', touch, false)

    function touch(event) {
      switch (event.type) {
        case 'touchstart':
          console.log(event.touches[0].clientX + ',' + event.touches[0].clientY)
        case 'touchend':
          console.log(event.changedTouches[0].clientX + ',' + event.changedTouches[0].clientY)
          break
        case 'touchmove':
          console.log(event.touches[0].clientX + ',' + event.touches[0].clientY)
          break
      }
    }
}
window.addEventListener('load', load, false)

当手指点触屏幕上的元素时,依次会发生如下事件(包括鼠标事件):

  • (1) touchstart
  • (2) mouseover
  • (3) mousemove(1 次)
  • (4) mousedown
  • (5) mouseup
  • (6) click
  • (7) touchend

5. pointer-event

在 PC 时代,我们通过鼠标与屏幕交互,这时候,我们设计系统时只需要考虑鼠标事件就好了。但是如今,有很多新的设备,比如智能手机,平板电脑,他们包含了其他的输入方式,比如触摸,手写笔,官方也为这些输入形式都提供了新的事件。

但是对于开发者来说,这是件很麻烦的事,因为这意味着你需要为你的网页适配各种事件,比如你要根据用户的移动来画图,你需要兼容 PC 和手机,你的代码可能就会是下面这样

dom.addEventListener('mousemove',draw);
dom.addEventListener('touchmove',draw);

为了解决这一系列的问题,W3C 定义了一种新的输入形式,即 pointer。任何由鼠标、触摸、手写笔或者其他输入设备在屏幕上触发的接触,都算是 pointer 事件。

  • pointerover:与 mouseover 行为一致
  • pointerenter:与 mouseenter 行为一致
  • pointerdown:指针进入活动状态,比如触摸了屏幕,类似于 touchstart
  • pointermove:指针进行了移动
  • pointerup:指针取消活动状态,比如手指离开了屏幕,类似于 touchend
  • pointercancel:类似于 touchcancel
  • pointerout:指针离开元素边缘或者离开屏幕,类似于 mouseout
  • pointerleave:类似于 mouseleave
  • gotpointercapture:元素捕获到指针事件时触发
  • lostpointercapture:指针被释放时触发 用法如下:
div.addEventListener('pointerdown', event => {
  handlePressDown()
})

event对象有如下属性:

  • pointerId:当前指针事件的唯一标识,主要是在多点触控时标识唯一的一个输入源
  • width:接触面的宽度
  • height:接触面的高度
  • pressure:接触的压力值,范围是0-1,对于不支持压力的硬件,比如鼠标,按压时该值必须为 0.5,否则为 0
  • tiltXtitltY:手写笔的角度
  • pointerType:事件类型,目前有 mousepentouch,如果是无法探测的指针类型,则该值为空字符串
  • isPrimary:用于标识是否是主指针,主要是在多点触控中生效,开发者也可以通过忽略非主指针的指针事件来实现单点触控。
    如何确定主指针:
    • 鼠标输入:一定是主指针
    • 触摸输入:如果 pointerdown 触发时没有其他激活的触摸事件,isPrimary 为 true
    • 手写笔输入:与触摸事件类似,pointerdown 触发时没有其他激活的 pointer 事件