DOM 事件

DOM 一共分为四个级别:DOM0 级,DOM1 级,DOM2 级和 DOM3 级
DOM 事件有三种:DOM0 级事件处理,DOM2 级事件处理和 DOM3 级事件处理
如下图:

1.jpg

DOM0 级事件

<button onclick="func">点我</button>
<script>
  function func() {
    console.log(1)
  }
</script>

这是 DOM0 之前 HTML 的事件处理,是最早的一种事件处理方式,也是最不推荐的一种。因为标签内事件所触发的函数名称和 JS 中的函数有强烈的耦合性,一旦函数名称修改,也必须修改 html 所触发的事件名,非常麻烦,但这种方法可以不需要操作 DOM 就完成事件的绑定。

DOM0 级事件处理就是将函数赋给事件处理属性

const btn = document.getElementById('btn')
btn.onclick = function () {
  console.log(1)
}

这种处理方式的优点是简单,具有跨浏览器的优势,所有浏览器都支持这种写法。如果想解绑事件可以使用btn.onclick = null来解绑事件。DOM0 事件处理的缺点也很明显,就是无法绑定多个处理函数,于是有了 DOM2 级事件处理。

DOM2 级事件

DOM2 级事件弥补了 DOM0 级事件无法绑定多个处理函数的缺点:

const btn = document.getElementById('btn')
function func() {
  console.log(1)
}
btn.addEventListener('click', func, false)
btn.addEventListener('mouseenter', func, true)

// 解绑事件
btn.removeEventListener('click', func, false)
btn.removeEventListener('mouseenter', func, true)

DOM2 级事件通过addEventListener方法监听事件的触发,想要解绑事件可以通过removeEventListener来解绑事件。

Notice

如果同一个监听事件分别为“事件捕获”和“事件冒泡”注册了一次,这两次事件需要分别移除。两者不会互相干扰。移除捕获监听器不会影响非捕获版本的相同监听器,反之亦然。

DOM3 级事件

DOM3 级事件并没有新增绑定事件的方法,而是添加了许多事件类型:

  1. 变动事件,当底层 DOM 结构发生变化时触发,如:DOMsubtreeModified
  2. UI 事件,当用户与页面上的元素交互时触发,如:load、scroll
  3. 焦点事件,当元素获得或失去焦点时触发,如:blur、focus
  4. 鼠标事件,当用户通过鼠标在页面执行操作时触发如:dbclick、mouseup
  5. 滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel
  6. 文本事件,当在文档中输入文本时触发,如:textInput
  7. 键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydown、keypress
  8. 合成事件,当为 IME(输入法编辑器)输入字符时触发,如:compositionstart

DOM 事件流

这里着重说一下addEventListener这个方法,addEventListener接受三个参数,第一个是事件名称,第二个是事件处理函数,第三个布尔值,表示事件在何时执行。
true代表事件在捕获阶段执行,false代表事件在冒泡阶段执行。

事件捕获和事件冒泡如下图所示: 1.jpg

事件捕获

事件捕获是自上而下执行,首先windows会捕获到事件,然后html会捕获到,接着是body、最后是div捕获到。

举个栗子:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <div id="div1">
      <div id="div2">div2</div>
    </div>
    <script>
      const oDiv1 = document.getElementById('div1')
      const oDiv2 = document.getElementById('div2')
      oDiv1.addEventListener(
        'click',
        () => {
          console.info(2)
        },
        true
      )
      oDiv2.addEventListener(
        'click',
        () => {
          console.info(1)
        },
        true
      )
    </script>
  </body>
</html>

点击div2,最后控制台输出的是2、1,这是因为发生了事件捕获,在div1捕获到事件的时候会触发自身的click事件,等到div2捕获到事件的时候才会触发div2click事件。

事件冒泡

事件冒泡和事件捕获恰恰相反,是自下而上执行:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <div id="div1">
      <div id="div2">div2</div>
    </div>
    <script>
      const oDiv1 = document.getElementById('div1')
      const oDiv2 = document.getElementById('div2')
      oDiv1.addEventListener(
        'click',
        () => {
          console.info(2)
        },
        false
      )
      oDiv2.addEventListener(
        'click',
        () => {
          console.info(1)
        },
        false
      )
    </script>
  </body>
</html>

点击div2,最后控制台输出的是1、2,这是因为发生了事件冒泡,在div2被点击之后出发自身的click事件,然后事件会冒泡到div1,触发div1click事件。

了解了事件捕获和事件冒泡,做道题试一下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <div id="div1">
      <div id="div2">
        <div id="div3">div3</div>
      </div>
    </div>
    <script>
      const oDiv1 = document.getElementById('div1')
      const oDiv2 = document.getElementById('div2')
      const oDiv3 = document.getElementById('div3')
      oDiv1.addEventListener(
        'click',
        () => {
          console.info(1)
        },
        false
      )
      oDiv1.addEventListener(
        'click',
        () => {
          console.info(2)
        },
        true
      )
      oDiv2.addEventListener(
        'click',
        () => {
          console.info(3)
        },
        false
      )
      oDiv2.addEventListener(
        'click',
        () => {
          console.info(4)
        },
        true
      )
      oDiv3.addEventListener(
        'click',
        () => {
          console.info(5)
        },
        false
      )
      oDiv3.addEventListener(
        'click',
        () => {
          console.info(6)
        },
        true
      )
    </script>
  </body>
</html>

点击div3,会输出什么?
答案是: 2、4、5、6、3、1
下面来分析一下步骤:

  1. div1 捕获事件发生,输出 2
  2. div2 捕获事件发生,输出 4
  3. div3 冒泡事件发生,输出 5
  4. div3 捕获事件发生,输出 6
  5. div2 冒泡事件发生,输出 3
  6. div1 冒泡事件发生,输出 1

这里就有疑问了,不应该是先捕获,再冒泡的么,为什么这里是先执行冒泡呢?

因为我们点击的是div3,也就是说在div1div2捕获完成的时候,已经处于事件目标阶段,而不是事件冒泡阶段,这个时候,在绑定捕获代码之前写了绑定的冒泡阶段的代码,所以在目标元素上就不会遵守先发生捕获后发生冒泡这一规则,而是先绑定的事件先发生。按照从上到下的顺序,先绑定的事件就先执行。而不是按照捕获和冒泡的顺序。

事件委托

事件委托就是利用冒泡的原理,把事件加到父元素或祖先元素上,触发执行效果。

事件委托有什么优点呢?假如ul中又 100 个li标签,我需要在点击li的时候获取该li的文本内容,那么我就要利用for循环给每一个li添加一个click事件,操作DOM本身就是一个消耗资源的操作,加上for循环,简直就是噩梦。访问DOM的次数越多,引起浏览器重回与回流的次数就越多,如何减少DOM操作?答案是事件委托

看下面的代码:

<ul id="list">
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
  <li>6</li>
  <!--...-->
</ul>
<script>
  ;(function () {
    const list = document.getElementById('list')
    list.addEventListener('click', showText, false)
    function showText(e) {
      const x = e.target
      if (x.nodeName.toLowerCase() === 'li') {
        alert('The color is ' + x.innerText)
      }
    }
  })()
</script>

利用事件冒泡机制,只要点击li就会触发ul上的click事件,事件处理函数接受一个参数eventevent是一个对象,它提供了参数targettarget就可以表示当前事件操作的DOM,但仅仅是可以表示而已,因为它不是真正的DOM,可以用nodeName来获取标签名,由于标签名是大写的,所以转换成小写(便于查看),这样就只会在点击 li 的时候触发该事件了。

事件委托相比原始的for循环事件绑定大大减少了DOM操作,只需要监听一个ul就能达到效果。