# DOM 事件的级别

DOM级别目前有四种:DOM0、DOM1、DOM2、DOM3。事件级别有三种 DOM0、DOM2、DOM3,因为 DOM1 没有涉及和事件相关的东西,所以没有 DOM1 级事件。

# DOM0 级事件

DOM0 级事件可以直接在标签上添加。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button onclick="handleClick()">DOM0</button>
  <script>
    function handleClick() {
      console.log('DOM1 事件')
    }
  </script>
</body>
</html>

也可以通过 JS 添加事件。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button id="btn">DOM0</button>
  <script>
    btn.onclick = function() {
      console.log('DOM0 事件')
    }
  </script>
</body>
</html>

想要移除 DOM0 事件,需要获取到绑定事件的 DOM 节点,然后将相应事件赋值为 null 。

ele.onclick = null

如果我们给同一元素绑定了多个相同的 DOM0 事件会怎么样呢?

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button id="btn">DOM0</button>
  <script>
    // DOM1 绑定多个相同事件,只会执行最后一个事件
    btn.onclick = function() {
      console.log('1');
    }
    btn.onclick = function() {
      console.log('2');
    }
    btn.onclick = function() {
      console.log('3');
    }
  </script>
</body>
</html>

当点击 DOM0 按钮后,控制台可以看到只输出了 3,并没有触发 12。但是在开发过程中我们就是需要给一个事件绑定两个方法改怎么办呢,比如我就想输出 123,咋办?我们可以像下面这样实现。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button id="btn">DOM0</button>
  <script>
    btn.onclick = function () {
      log1()
      log2()
      log3()
    }

    function log1() {
      console.log('1')
    }

    function log2() {
      console.log('2')
    }

    function log3() {
      console.log('3')
    }
  </script>
</body>
</html>

DOM0 级事件是在冒泡阶段触发的。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button id="btn">DOM0</button>
  <script>
    window.onclick = function() {
      console.log('window')
    }
    document.onclick = function() {
      console.log('document')
    }
    document.documentElement.onclick = function() {
      console.log('html')
    }
    btn.onclick = function() {
      console.log('btn')
    }
  </script>
</body>
</html>

上面的列子中,当我们点击 DOM0 按钮时,浏览器控制台会依次打印出:

btn
html
document
window

# DOM2 级事件

DOM2 事件指定了两个方法用于事件的绑定和解绑:addEventListener(event, function, useCapture)removeEventListener(event, function, useCapture) 。这两个方法接收三个参数,其中第三个参数可选。

  • 第一个参数为事件名,如 'click'
  • 第二个参数为事件触发时执行的回调函数
  • 第三个参数是个 bool,用于指定事件是在捕获阶段触发还是在冒泡阶段触发,默认为 false,事件在冒泡阶段触发。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button id="btn">DOM2</button>
  <script>
    // DOM2 绑定多个相同事件,会依次触发
    btn.addEventListener('click', function() {
      console.log('dom2 触发1')
    })
    btn.addEventListener('click', function() {
      console.log('dom2 触发2')
    })
    btn.addEventListener('click', function() {
      console.log('dom2 触发3')
    })
  </script>
</body>
</html>

假设我们想 触发2 的时候事件就停止,不执行 触发3,该怎么办呢?我们可以使用 event.stopImmediatePropagation() 来阻止事件的传播。如果多个事件监听器被附加到相同元素的相同事件类型上,当此事件触发时,它们会按其被添加的顺序被调用。如果在其中一个事件监听器中执行 stopImmediatePropagation() ,那么剩下的事件监听器都不会被调用。



















 








<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button id="btn">DOM2</button>
  <script>
    // DOM2 绑定多个相同事件,会依次触发
    btn.addEventListener('click', function() {
      console.log('dom2 触发1')
    })
    btn.addEventListener('click', function(e) {
      console.log('dom2 触发2')

      e.stopImmediatePropagation();
    })
    btn.addEventListener('click', function() {
      console.log('dom2 触发3')
    })
  </script>
</body>
</html>

再次点击 DOM2 按钮,可以看到 触发3 事件没有被执行。

但是上面那样添加的方法无法解绑,如果想解绑的话,回调函数需要是一个具名函数。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button id="btn">DOM2</button>
  <script>

    function handleClick() {
      console.log('触发了事件');
    }

    // 添加事件
    btn.addEventListener('click', handleClick)
    
    setTimeout(() => {
      // 移除事件
      btn.removeEventListener('click', handleClick)
    }, 5000);
  </script>
</body>
</html>

在 IE8 及之前的 IE 版本中是不支持 addEventListenerremoveEventListener 。需要使用 attachEvent(event, function)detachEvent(event, function)

# DOM3 级事件

DOM3 在事件定义方式没发生什么大的改变,只不过事件的类型增加了很多,如鼠标事件、键盘事件等等。

ele.addEventListener('keyup', function(){}, false)

举例:

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

# DOM 事件模型

DOM事件模型分为两种:事件冒泡(由IE团队提出)和事件捕获(由网景团队提出)。

# 事件冒泡

事件被定义为从最具体的元素(文档树中最深的节点)开始触发,然后向上传播至没有那么具体的元素(文档)。

# 事件捕获

事件捕获的意思是最不具体的节点应该最先收到事件,而最具体的节点应该最后收到事件。事件捕获实际上是为了在事件到达最终目标前拦截事件。

由于旧版本浏览器不支持,因此实际当中几乎不会使用事件捕获。通常建议使用事件冒泡,特殊情况下可以使用事件捕获。

# DOM 事件流

事件流描述了页面接收事件的顺序。DOM2 Events规范规定事件流分为3个阶段:

事件捕获 -> 目标阶段 -> 事件冒泡

# DOM 事件捕获的具体流程

window -> document -> html(document.documentElement获取 html 标签) -> body -> 一层层向下传 -> 目标元素,然后一层层向上冒泡

# Event 对象的常见应用

这里简单列举几个在开发中常用到的 Event 对象的功能,更多细节请查阅 MDN 文档 Event (opens new window)

// 阻止默认事件
e.preventDefault()
// 阻止冒泡
e.stopPropagation()
// 阻止监听同一事件的其他事件监听器被调用
e.stopImmediatePropagation()
// 当前绑定事件的对象
e.currentTarget
// 表示当前被点击的元素
e.target

# 自定义事件

# Event

作用: Event() 构造函数, 创建一个新的事件对象 Event。

语法:

event = new Event(typeArg, eventInit);

参数:

  • typeArg,不可选,表示所创建事件的名称
  • eventInit,可选,字典对象,包含:
字段名 含义 是否可选 类型 默认值
bubbles 表示该事件是否冒泡 可选 Boolean false
cancelable 表示该事件能否被取消 可选 Boolean false
composed 事件是否会在影子DOM根节点之外触发侦听器 可选 Boolean false

示例:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    // customEvent - 创建的事件名,按需取名即可
    const event = new Event('customEvent');

    document.addEventListener('customEvent', function() {
      console.log('触发了自定义事件')
    })
    
    // 触发事件, document可以换成任何其他 DOM 元素
    document.dispatchEvent(event)
  </script>
</body>
</html>

更多细节请阅读 MDN 文档 Event() (opens new window)

# CustomEvent

作用: 创建一个新的 CustomEvent 对象

语法:

event = new CustomEvent(typeArg, customEventInit);

参数:

  • typeArg - event 名字的字符串
  • customEventInit - 可选的字典参数,包括:
字段名 含义 默认值 类型
detail 可以用于传递参数 null any
bubbles 表示事件是否冒泡 false Boolean
cancelable 表示事件是否可以取消 false Boolean

示例:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <script>
    var event = new CustomEvent("cat", {
      detail: '123'
    });
    document.addEventListener('cat', function (e) {
      console.log(e.detail) // '123'
    })

    document.dispatchEvent(event)
  </script>
</body>

</html>

更多细节请阅读 MDN 文档 CustomEvent() (opens new window)

# Event 和 CustomEvent 对比

使用 CutomeEvent 创建的自定义事件可以通过 detail 属性传递自定义参数,而使用 Event 创建的自定事件不可以传递自定义参数。

# 使用场景

实在抱歉哈,在目前所开发的项目中暂时还未用到必须使用自定义事件才能完成的任务,暂时不知道怎么落地,后续有相应需求后再做更新。

# 练手

使用面向对象的方式维护一个列表,每个列表有一个删除按钮,单击删除按钮移除当前行。

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title></title>
  <style media="screen">
    .del {
      padding: 10px;
    }
  </style>
</head>

<body>
  <ul class="list">
    <li>第一行<span class="del">x</span></li>
    <li>第二行<span class="del">x</span></li>
    <li>第三行<span class="del">x</span></li>
    <li>第四行<span class="del">x</span></li>
    <li>第五行<span class="del">x</span></li>
  </ul>
  <ul class="list">
    <li>第一行<span class="del">x</span></li>
    <li>第二行<span class="del">x</span></li>
    <li>第三行<span class="del">x</span></li>
    <li>第四行<span class="del">x</span></li>
    <li>第五行<span class="del">x</span></li>
  </ul>
  <script type="text/javascript">
    class List {
      constructor(sel) {
        this.el = Array.from(document.querySelectorAll(sel));
        let self = this;
        this.el.forEach(item => {
          item.addEventListener('click', function (e) {
            if (e.target.className.indexOf('del') > -1) {
              self.removeItem.call(self, e.target);
            }
          });
        });
      }
      removeItem(target) {
        let self = this;
        let findParent = function (node) {
          let parent = node.parentNode;
          let root = self.el.find(item => item === parent);
          if (root) {
            root.removeChild(node);
          } else {
            findParent(parent);
          }
        };
        findParent(target);
      }
    }

    window.addEventListener('DOMContentLoaded', function () {
      new List('.list');
    });
  </script>
</body>

</html>

界面展示如下:

# 参考

《JS 高级程序设计第四版》