DOM 级别、事件流(事件冒泡、捕获)、事件委托原理以及自定义事件

807 阅读7分钟

DOM 介绍

DOM(Document Object Model——文档对象模型)wikipedia.org 是用来呈现以及与任意 HTML 或 XML文档交互的API。DOM 是载入到浏览器中的文档模型,以节点树的形式来表现文档,每个节点代表文档的构成部分(例如:页面元素、字符串或注释等等)。

DOM 是万维网上使用最为广泛的 API 之一,因为它允许运行在浏览器中的代码访问文件中的节点并与之交互。节点可以被创建,移动或修改。事件监听器可以被添加到节点上并在给定事件发生时触发。

DOM 并不是天生就被规范好了的,它是浏览器开始实现 JavaScript 时才出现的。这个传统的 DOM 有时会被称为 DOM 0。**现在, WHATWG 维护 DOM 现存标准。**这个标准是动态标准(Living Standard),表示谁时都会有新的标准细则地添加和变更。DOM Standard (whatwg.org)

DOM 事件

DOM 级别

DOM 级别一共可以分为四个级别:DOM0 级、DOM1 级、DOM2 级、DOM3 级和 DOM4 级(动态标准)。其中 DOM1 级中没有事件的相关内容。

级别用法注意事项
DOM0el.onclick=function(){}
<img onclick="console.log('click')">
1. 绑定多个同类型事件将会被覆盖;
2. 事件处理函数执行阶段都是冒泡阶段或者目标阶段;
3. 通过给事件处理属性赋值null来解绑事件;
4. 事件处理函数如果返回 false,则会阻止自身的默认行为,如 a 标签跳转行为,表单提交行为,⚠️注意:不会阻止事件传播!
5. event.preventDefault()也可阻止默认行为。
6. event.stopPropagation()只能阻止事件冒泡。
DOM1DOM 1 级别于 1998 年 10 月 1 日成为 W3C 推荐标准。1 级 DOM 标准中并没有定义事件相关的内容,所以没有所谓的 1 级 DOM 事件模型。
DOM2addEventListener
removeEventListener
1. 可以绑定多个同类型事件;
2. 统一了事件流模型“捕获=>目标=>冒泡”;
3. 根据事件流的传播机制,可在目标元素的祖先节点注册相同类型事件,进行提前处理。
4.addEventListener传入的第三个参数 为true,表示处理函数在捕获阶段执行。
5.addEventListener的第三个参数可以传递对象,表示配置项 具体参考MDN
6. 使用event.stopPropagation()阻止事件传播。
DOM3在 DOM 2 级事件的基础上添加了更多的事件类型。同时 DOM 3 级事件也允许使用者自定义一些事件。
1. UI 事件,当用户与页面上的元素交互时触发,如:load、scroll
2. 焦点事件,当元素获得或失去焦点时触发,如:blur、focus
3. 鼠标事件,当用户通过鼠标在页面执行操作时触发如:dblclick、mouseup
4. 滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel
5. 文本事件,当在文档中输入文本时触发,如:textInput
6. 键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydown、keypress
7. 合成事件,当为IME(输入法编辑器)输入字符时触发,如:compositionstart
8. 变动事件,当底层DOM结构发生变化时触发,如:DOMsubtreeModified
DOM41. 添加了对 CustomEvent 构造函数的支持。
......

事件流

DOM 事件标准描述了事件传播的 3 个阶段:

  1. 捕获阶段(The capture phase)—— 事件对象从目标的祖先Window传播到目标的父节点过程。
  2. 目标阶段(The target phase)—— 事件传播到目标节点上。
  3. 冒泡阶段(The bubble phase)—— 事件对象从目标的父节点开始向上传播,直到Window

同一个元素既可以在捕获阶段处理也可以在冒泡阶段处理;

DOM0 级(onclick)基本上只能控制冒泡阶段,而 DOM2(addEventListener)级是可以控制捕获阶段的;

事件对象 Event

因为各个浏览器的事件对象不一样, 把主要的事件对象的属性和方法列出来;

  • bubble : 返回一个布尔值,表明当前事件是否会向DOM树上层元素冒泡
  • cancelable : 表明是否可以取消冒泡
  • currentTarget : 当前事件程序正在处理的元素, 和this一样的
  • defaultPrevented:如果调用了preventDefualt则为true
  • detail: 与事件有关的信息(滚动事件等等)
  • eventPhase: 值为1表示处于捕获阶段, 值为2表示处于目标阶段,值为3表示在冒泡阶段
  • target: 事件目标,即绑定事件的元素
  • trusted: 为ture是浏览器生成的,为false是开发人员创建的(DOM3)
  • type: 事件的类型 view,与元素关联的window, 我们可能跨iframe
  • preventDefault():取消默认事件,如 a 链接跳转,表单提交。但不会阻止事件传播。
  • stopPropagation():阻止当前事件的进一步传播。但是不阻止默认行为。
  • stopImmediatePropagation():阻止当前事件的进一步传播,并且阻止当前元素剩下的同类事件处理函数执行。

控制事件传播和默认行为

  • DOM0 事件处理函数可以通过return false来阻止默认事件(但无法阻止事件传播)。

    <!-- 内联html事件处理 -->
    <a href="http://www.baidu.com" onclick="return false;"></a> 
    <!--可以阻止事件默认行为👍,但无法阻止事件传播-->
    
    <a href="http://www.baidu.com" onclick="handleClick()">baidu</a>
    
    <script>
    document.getElementsByTagName("a")[0].onclick(event){
    event.preventDefault() //🚫阻止默认事件
    event.stopPropagation() //🚫阻止事件传播
    return false // //🚫阻止默认事件但无法阻止事件传播
    }
    </script>
    
  • DOM2 事件处理

    • 通过target.addEventListener(type, listener, useCapture);给 DOM 节点绑定事件监听函数

    • type:表示监听事件类型的字符串。

    • listener:监听函数。

    • useCapture:布尔值/对象,表示是否在捕获阶段执行监听函数。默认值为false

    • listener函数默认接收一个事件对象参数event,可以在函数内部调用 event.stopPropagation() 来停止事件传播(捕获冒泡阶段都可以🤞)。

       <a href="http://www.baidu.com">baidu</a>
       
       <script>
        document.getElementsByTagName('a')[0].addEventListener('click', (event) => {
         event.preventDefault() //🚫阻止默认事件,a 链接跳转行为
         event.stopPropagation() //🚫阻止事件往下传播
       },true)
       </script>
    

为什么需要事件流

提问:为何不直接触发事件对象上的处理函数?

思考:比如在点击事件中,如果点击事件绑定在父节点上,而子节点的宽高超过了父节点,那岂不是永远触发不了父节点上的点击事件处理函数了。由此可见,我们需要事件流。(当然这只是我猜测的一种原因)

事件委托

利用 DOM 事件的传播机制,事件最后都会传播到祖先元素上(没有手动停止事件传播),我们就可以给祖先元素绑定一个点击事件,在事件中我们只需要获取事件目标(event.target,表示触发事件的节点);根据不同的事件目标做不同的处理就可以的了,这样就可以不用给每个子元素单独绑定事件的了。

优点:节省代码量和内存。

简单版本

  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li data-flag="special">4</li>
    <li>5</li>
    <li>6</li>
  </ul>
  
<script>
  document.querySelector('ul').addEventListener('click',(e)=>{
    if(e.target.dataset.flag==='special'){
      alert('I am special!') 
      //do something...
    }
  })
</script>

优化版本

考虑到了li内部元素触发事件时的监听

  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li data-flag="special">
      <span>title</span>
      <span>content</span>
    </li>
    <li>5</li>
    <li>6</li>
  </ul>
  
<script>
document.querySelector('ul').addEventListener('click', (e) => {
  if (e.target.closest('[data-flag="special"]')) {
    alert('I am special!')
  }
})
</script>

DOM 自定义事件

构造函数语法参数返回值注意
Eventnew Event(typeArg, eventInit)typeArg:string 类型,事件名称。
eventInit:可选,传入一个对象。
一个新的Event对象不可传递自定义参数
CustomEventnew CustomEvent(typeArg, customEventInit)typeArg:string 类型,事件名称。
CustomEventInit:可选,传入一个对象。
一个新的CustomEvent对象可通过CustomEventInit.detail字段传递自定义参数

EventInit 类型字典参数:

  • bubbles,可选,默认值为 false,表示该事件是否冒泡。
  • cancelable,可选,默认值为 false, 表示该事件能否被取消。
  • composed,可选,,默认值为 false,指示事件是否会在影子DOM根节点之外触发侦听器。

CustomEventInit类型字典参数。

  • bubbles:一个布尔值,表明该事件是否会冒泡。
  • cancelable:一个布尔值,表明该事件是否可以被取消。
  • detail:当事件初始化时传递的数据。

CustomEvent 示例

// 添加一个适当的事件监听器
eventTarget.addEventListener("cat", function(e) { process(e.detail) })

// 创建并分发事件
var event = new CustomEvent("cat", {"detail":{"hazcheeseburger":true}})
eventTarget.dispatchEvent(event)

参考

  1. www.w3.org/TR/DOM-Leve…
  2. juejin.cn/post/684490…
  3. www.jianshu.com/p/bbd6e600c…
  4. segmentfault.com/a/119000002…