Featured image of post DOM 事件与事件委托

DOM 事件与事件委托

什么是 DOM 事件

文档对象模型 (Document Object Model) 是 HTML 和 XML 文档的编程接口。

它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。

文档对象模型(DOM)提供了对同一份文档的另一种表现,存储和操作的方式。 DOM 是 web 页面的完全的面向对象表述,它能够使用如 JavaScript 等脚本语言进行修改。

而事件是 JavaScript 与 HTML 交互的基础。要实现用户与页面的交互,先要通过 DOM 接口访问元素,再对目标元素绑定特定的事件、设置事件处理函数,然后用户触发事件,事件处理函数执行,产生交互效果。

DOM 事件模型和事件流

事件流又称为事件传播,描述的是从页面中接收事件的顺序。当一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。

  1. 事件捕获阶段(capturing phase): 事件从 window 对象自上而下向目标节点传播的阶段;
  2. 处于目标阶段(target phase): 真正的目标节点正在处理事件的阶段;
  3. 事件冒泡阶段(bubbling phase): 事件从目标节点自下而上向 window 对象传播的阶段。

 

在DOM2级事件流中,当我们点击下面的代码片段中的 div 元素后,在事件捕获阶段从上到下传播

window –> document -> html -> body –> 目标

1
2
3
4
5
<html>
    <body>
        <div> 点我 </div>
    </body>
</html> 

而事件冒泡的流程刚好是事件捕获的逆过程,冒泡和捕获的过程如下图: DOM 事件流模型

下面来具体看看事件捕获和事件冒泡。

事件捕获

Netscape 提出的事件流模型称为事件捕获。

事件从最不具体的节点开始接收(document),传递至最具体的节点 ,和 IE 的冒泡相反, 事件捕获的本意是当事件到达预定目标前捕获它。

当我们点击下面的代码片段中的 div 元素时,单击事件会按照如下顺序传播:

document –> html –> body –> div

1
2
3
4
5
<html>
    <body>
        <div> 点我 </div>
    </body>
</html> 

事件冒泡

IE的事件流称为事件冒泡。

即:事件由最具体的元素接收(div),逐级向上传播到不具体的节点(document)。

当我们点击下面的代码片段中的 div 元素时,单击事件会按照如下顺序传播:

div –> body –> html –> document

1
2
3
4
5
<html>
    <body>
        <div> 点我 </div>
    </body>
</html> 

几乎现代所有的浏览器都支持事件冒泡,不过有一些细微的差别

  • IE5.5 和 IE5.5 - 版本的事件冒泡会跳过 html 元素(body 直接到 document)
  • IE9、Firefox、Chrome、Safari则一直冒泡到 window 对象。

事件委托/事件代理

传统的事件处理中,需要为每个元素添加事件处理器。而由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。

事件代理的原理用到的就是事件冒泡和目标元素,把事件处理器添加到父元素,等待子元素事件冒泡,并且父元素能够通过 target(IE为 srcElement)判断是哪个子元素,从而做相应处理。

事件代理的优点

> 减少内存消耗,提高性能

假设有一个列表,给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。而将多个事件处理器减少到一个,因为事件处理器要驻留内存,这样就提高了性能。

借助事件代理,我们只需要给父容器 ul 绑定方法即可,这样不管点击的是哪一个后代元素,都会根据冒泡传播的传递机制,把容器的绑定的事件触发,然后把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而完成不同的事。

> 动态绑定事件

在很多时候,我们需要通过用户操作动态的增删列表项元素,如果一开始给每个子元素绑定事件,那么在列表发生变化时,就需要重新给新增的元素绑定事件,给即将删去的元素解绑事件,如果用事件代理就会省去很多这样麻烦。

事件绑定API

1
2
3
4
5
6
7
8
//IE 5,冒泡(从小到大)
div.attachEvent(`onclick`, fn)

//网景,捕获(从大到小)
div.addEventListener(`onclick`, fn)

//W3C,先捕获再冒泡
div.addEventListener(`click`, fn, true)

> addEventListener 的第三个参数

网景 和 微软 曾经的战争还是比较火热的,当时, 网景主张 捕获 方式,微软主张 冒泡 方式。后来 W3C 采用折中的方式,平息了战火,制定了统一的标准 —— 先捕获再冒泡

addEventListener 的第三个参数就是为冒泡和捕获准备的。 addEventListener有三个参数:

1
2
3
4
5
element.addEventListener(event, function(){}, useCapture)

//第一个参数是需要绑定的事件
//第二个参数是触发事件后要执行的函数
//第三个参数不传或者为 falsy 表示在事件冒泡阶段调用事件处理函数;如果参数为true,则表示在事件捕获阶段调用处理函数。

阻止默认事件/阻止冒泡

阻止默认事件

浏览器的默认事件指的就是浏览器自己的行为。

  • 比如我们在点击 <a href="#"> 的时候,浏览器跳转到指定页面。
  • 当我们滚动鼠标或按空格键和按方向键时页面会向下滚动。

有时候为了更好的用户体验,这时我们就需要阻止浏览器的默认行为。

1
2
3
4
5
6
7
8
let stopDefault = (e) => {
    if ( e && e.preventDefault ){
        e.preventDefault(); //阻止默认浏览器动作(W3C)
    }else {
        window.event.returnValue = false; //IE中阻止函数器默认动作的方式
}
return false;
}

阻止冒泡

上面了解了捕获和冒泡的流程,那么在有些情况下我们触发事件后不想让事件继续向上冒泡怎么办?

答案是使用 e.stopPropagation() 方法,当触发了这行代码后,事件将停止冒泡。

具体看一个例子 ⬇️ ⬇️ ⬇️

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<div id="box">
    <svg id="icon">
    </svg>
</div>

<script>
    box.addEventListener(`click`, () => {
        console.log(`点击了一下盒子`)
    })
    
    icon.addEventListener(`click`, (e) => {
        e.stopPropagation()    //关键代码,阻止冒泡
        console.log(`点击了一下icon`)
    })
</script>

一个 div 里面有一个图标 icon,当用户点击 div 内除了图标的区域时,打印 “点击了一下盒子”,当用户点击 icon 时,打印 “点击了一下icon” 且不打印 “点击了一下盒子”。

资料来源:饥人谷、MDN、DOM事件机制