DOM事件模型(事件流)

2,124 阅读5分钟

DOM

首先,DOM全称是Document Object Model,即文档对象模型。DOM是W3C的标准,定义了访问 HTML 和 XML 文档的标准。

DOM事件

事件是用户或者浏览器自己执行的某种动作,是文档或者浏览器发生的一些交互瞬间,比如点击(click)按钮等,这里的click就是事件的名称。JS与html之间的交互是通过事件实现的,DOM支持大量的事件。

DOM事件机制(事件流)

DOM的结构是一个树形,每当HTML元素产生事件时,该事件就会在树的根节点和元素节点之间传播,所有经过的节点都会收到该事件。DOM事件流的出现是在DOM节点中事件发生时常见的一种现象中产生的,如下问题

<div class="爷爷">
    <div class="爸爸">
        <div class="儿子">
            文字
        </div>
    </div>
</div>

假如我分别给爷爷、爸爸、儿子绑定fnYe、fnBa、fnEr三种点击事件。那么,当我点击儿子里面的文字时,会不会也点击了爸爸和爷爷? 答案是都算。 那么问题又来了,既然都点到了,那么先执行哪个,后执行哪个呢? 答案是都可以!!!

如何注册事件回调

DOM的事件模型(事件机制)主要反映在事件的注册和监听。通常,我们会通过以下几种方式来注册事件回调:

  1. 通过HTML属性
<button onclick="fn()">点击</button>
  1. 通过DOM元素属性
myButton.onclick = function(e) {console.log('button click')}
  1. 通过addEventListener为元素绑定事件回调
myButton.addEventListener('click',(e) => {console.log('button click!')})

但是现在不推荐使用HTML属性来设置事件回调,因为这种方式不能将内容/行为很好地分开,使得HTML变大并减少了可读性。

通过DOM元素属性的方式的缺点是每个事件元素只能被设置一个事件回调,在不需要设置多个事件回调的时候推荐使用这种办法,毕竟它比addEventListener少些很多个字符。

addEventListener
功能更高级,可以给DOM元素设置多个事件以及事件回调。但它也有个小缺点,IE6-8不兼容这个方法,在低版本的IE中需要使用attachEvent,为什么要说是小缺点呢,因为现在需要兼容IE的地方比较少了,甚至IE自己在自家最新的edge浏览器中都使用chrome的引擎了,而且如非必要,可以使用能解决跨浏览器兼容性的JS代码库来适应需求。

什么是DOM模型

知道如何绑定注册事件回调,更要清楚DOM的事件模型是怎样的

结论:DOM事件的模型(事件机制)就是事件捕获和事件冒泡。

最开始还没有形成标准的时候,各家浏览器都有各自的事件流机制。事件冒泡是IE浏览器支持的机制,而事件捕获是NetScape(网景)支持的。

后面根据W3C在2002年发布的标准,规定浏览器应该同时支持事件冒泡和事件捕获,首先事件以捕获(爷爷 => 父亲 => 儿子)的方式传递,然后以冒泡(儿子 => 父亲 => 爷爷)的方式传递。它们传递的时候会看是否有监听函数,有则调用并提供信息,没有就跳过。

具体的过程看图:

在有父子关系的元素中,先监听捕获,再监听冒泡;
当只有一个div,不存在父子关系的时候,谁先监听就先执行谁。

什么是事件流

事件传递的过程被称为事件流,它经历上面图中显示的三个阶段:捕获阶段、目标阶段、冒泡阶段。

事件的产生和传递一定按照捕获--> 目标 -->冒泡的方式执行吗?答案是否。

事件在开始调度之前,必须先确定事件对象的传播路径。即由程序员实现确定传播的方式是捕获还是冒泡。而且传播时的传播路径遵循HTML树结构。

程序员设置冒泡还是捕获通过addEventListener(eventType,listener,options)的第三个参数来设置,这个参数接收个可选参数对象:

addEventListener('click',fn,{
    capture:true, // 默认是false,开启捕获设置capture为true
    once:true   // 默认false,开启后listener参数只会被调用一次
})

如果 options为 true ,fn 就会走捕获阶段。即当浏览器进入捕获阶段发现 element 有 fn 监听函数,就会调用 fn 函数,并提供事件信息。

addEventListener('click',fn,true)

捕获是不可被取消的,但冒泡可以:

button.addEventListener('click',(e) => {
    console.log('button click')
    e.stopPropagation() // 这行代码可以阻止事件流的继续传递。
})

一般用于封装特定需求的独立组件。

target v.s currentTarget

区别

e.target 是用户操作的元素
e.currentTarget 是程序员监听的元素

特例:当只有一个 div 被监听,fn 分别在捕获阶段和冒泡阶段监听 click 事件

div.addEventListener('click', f1)
div.addEventListener('click', f2, true)

此时,谁先监听就会先执行。

自定义事件

element.addEventListener('click', () => {
    const event = new CustomEvent('custom', {
        "detail": {name: "custom", age: 18}
    })
    element.dispatchEvent(event)
})
element.addEventListener('custom', (e) => {
    console.log('custom')
    console.log(e)
})

总结

按照一般的逻辑,用户点击一个地方的时候,事件就应该从它开始往上传递,这更符合直觉,所以这也可能是W3C规定冒泡和捕获都支持,但函数addEventListener默认是冒泡的原因吧。
冒泡、捕获是一个非常有用的机制,我们可以用它来实现事件委托的功能。关于事件委托,将在下一篇博客介绍。