DOM事件模型&DOM事件机制

100 阅读9分钟

背景知识储备:DOM事件是浏览器提供的 DOM的功能,不属于JS的功能,JS与html之间的交互是通过事件实现的,JS只是调用了DOM提供的addEventListener而已。DOM事件是用户或者浏览器自己执行的某种动作,是文档或者浏览器发生的一些交互瞬间,DOM事件一方面是浏览器自带的100多种事件(可google 事件参考MDN),也可以开发者自定义事件。比较典型自带事件有:鼠标点击(click)按钮和鼠标滚轮(scroll)等,这里的click和scroll就是事件的名称,本文举例主要围绕这两个事件展开;也有一些是DOM支持自定义的事件。

事件

事件的本质是程序各个组成部分之间的一种通信方式,也是异步编程的一种实现。DOM支持大量的事件,事件操作有监听和触发。事件发生后,浏览器监听到了这个事件,就会执行对应的监听函数。

DOM节点监听事件是通过EventTarget接口,而且所有节点对象都部署了这个接口,包括其他需要事件通信的浏览器内置对象(XMLHttpRequest)也部署了EventTarget接口。

EventTarget接口的三个实例方法:


EventTarget.addEventListener();       //在当前节点或对象上 绑定事件的监听函数
EventTarget.removeEventListener();    //移除事件的监听函数
EventTarget.dispatchEvent();          //触发事件

绑定监听函数的方法

  • HTML的on-属性
<body onload="doSomething()">;  // on + 事件名  只在冒泡阶段触发
  • 元素节点对象的事件属性
window.onload = doSomething;   // 只会在冒泡阶段触发

div.onclick = function (event) {
  console.log('触发事件');
};
  • addEventListener方法
window.addEventListener('load', doSomething, false);  // 能够指定在捕获/冒泡阶段触发监听函数

事件的传播

一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。

  • 第一阶段:从window对象传导到目标节点(上层传到底层),称为“捕获阶段”(capture phase)。
  • 第二阶段:在目标节点上触发,称为“目标阶段”(target phase)。
  • 第三阶段:从目标节点传导回window对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。

这种三阶段的传播模型,使得同一个事件会在多个节点上触发。

1. 什么是DOM事件模型:

通俗一点来说就是一个事件被触发时,浏览器会自动从用户操作标签外的最上级标签逐渐向里检查是否有相同事件,如果有则触发,如果没有则继续向下检查直到用户操作的标签,这过程称为捕获,此时浏览器会继续由用户操作标签继续向上级标签检查,如果有相同事件则触发,如果没有则继续向上检查直到最上级元素为止,此过程称为冒泡。有监听函数就执行,并提供事件信息,没有就跳过。DOM事件模型有两种:事件冒泡和事件捕获。

1.1 DOM 事件流

当一个HTML元素(比如div)触发一个事件时,该事件会在元素结点(这个div)与根结点之间的路径传播,这个传播过程就是DOM 事件流;DOM事件流的顺序分为三个阶段:捕获阶段、目标阶段、冒泡阶段。

也可以理解为:一个事件发生后,会在子元素及父元素之间进行传播propagation,这种传播分为三个阶段,这种三阶段的传播模型,使得同一个事件会在多个节点上触发:

  1. 由外向内找监听函数就是事件捕获
  2. 在目标节点触发事件
  3. 由内而外找监听函数就是事件冒泡

1.2 什么是捕获什么是冒泡,并说说是先捕获还是先冒泡

从内向外找监听函数,叫事件冒泡,有点像在水下呼吸,突出的泡泡逐渐上升变大; 从外向内找监听函数,叫事件捕获,有点像聚焦的过程,最后汇总在一个点上了; 开发者自己选择把监听函数放在捕获阶段还是放在冒泡阶段。

W3C事件模型:先捕获(由外向内找监听函数),再冒泡(由内向外找监听函数);两个事件可同时存在,也可以只有捕获,阻止冒泡。

1.3 事件冒泡

是指浏览器的事件流如同冒泡一样,从最里面到最外面。最里面对应的是DOM中最具体的元素比如,最外处则是最外层元素。开发者只需要将addEventListener的第三个参数改为false或不写第三个参数,使其为空,就可以实现事件冒泡。

1.4 事件捕获

捕获是从外到内,事件先从window对象,然后再到document对象,然后是html标签(通过document.documentElement获取html标签),然后是body标签(通过document.body获取body标签),然后按照普通的html结构一层一层往下传,最后到达目标元素。开发者自己选择把监听函数addEventListener的第三个参数改为true就可以实现事件捕获。

2. 什么是DOM事件机制

DOM 的事件操作(监听和触发),都定义在EventTarget接口。所有节点对象都部署了这个接口,其他一些需要事件通信的浏览器内置对象(比如,XMLHttpRequest、AudioNode、AudioContext)也部署了这个接口,该接口主要提供三个实例方法。

  • addEventListener:绑定事件的监听函数(有监听函数就调用,并提供事件信息,没有就跳过)
  • removeEventListener:移除事件的监听函数
  • dispatchEvent:触发事件
  • removeEventListener : 取消事件绑定
  • detachEvent: 取消事件绑定

EventTarget.addEventListener()用于在当前节点或对象上,定义一个特定事件的监听函数,

target.addEventListener(type, listener, useCapture)该方法接受三个参数:

type: 事件名称,大小写敏感,比如click listener: 监听函数。事件发生时,会调用该监听函数; useCapture: 布尔值,表示监听函数是否在捕获阶段capture触发,默认为false,或者可以不写这个参数,

baba.attachEvent('onclick', fn) // IE 5:冒泡
baba.addEventListener('click',fn) // 网景:捕获

baba.addEventListener('click', fn, bool)  //W3C

如果 bool 不传或为falsy,就让 fn 走冒泡,即当浏览器在冒泡阶段发现 baba 有 fn 监听函数,就会调用 fn,并提供事件信息;

如果 bool true,就让 fn 走捕获,即当浏览器在捕获阶段发现 baba 有 fn 监听函数,就会调用 fn ,并提供事件信息.

  1. 自定义事件(浏览器自带100多种事件) 事件参考
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
  <div id=div1> 
    <button id=button1>点击触发 frank 事件
     </button>
  </div>
</body>
</html>
button1.addEventListener('click',()=>{   //button1被点击时,监听触发frank事件
    const event = new CustomEvent('frank',{  
    //声明一个事件,自定义为名为frank,事件信息detail为: {name:'frank',age:18}
        detail: {name:'frank',age:18}
    })
    button1.dispatchEvent(event)  //触发事件
})

button1.addEventListener('frank',(e)=>{
    console.log(e.detail)
})

是否可以冒泡呢?

button1.addEventListener('click',()=>{   
    const event = new CustomEvent('frank',{  
        detail: {name:'frank',age:18},
        bubbles:true,   //自定义事件可以冒泡
        cancelable:false //不能阻止冒泡
    })
    button1.dispatchEvent(event)  
})

button1.addEventListener('frank',(e)=>{
    console.log(e.detail)
})

4. DOM事件模型代码实例:

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>

<body>
  <div class="level1 x">
    <div class="level2 x">
      <div class="level3 x">
        <div class="level4 x">
          <div class="level5 x">
            <div class="level6 x">
              <div class="level7 x">
                
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</body>

</html>
* {
  box-sizing: border-box;
}
div[class^=level] {
  border: 1px solid;
  border-radius: 50%;
  display: inline-flex;
}
.level1 {
  padding: 10px;
  background: purple;
}
.level2 {
  padding: 10px;
  background: blue;
}
.level3 {
  padding: 10px;
  background: cyan;
}
.level4 {
  padding: 10px;
  background: green;
}
.level5 {
  padding: 10px;
  background: yellow;
}
.level6 {
  padding: 10px;
  background: orange;
}
.level7 {
  width: 50px;
  height: 50px;
  border: 1px solid;
  background: red;
  border-radius: 50%;
}
.x{
  background: transparent; 
}

js代码的演进过程: 1.1 先定义:

const level1 = document.querySelector('.level1')
const level2 = document.querySelector('.level2')
const level3 = document.querySelector('.level3')
const level4 = document.querySelector('.level4')
const level5 = document.querySelector('.level5')
const level6 = document.querySelector('.level6')
const level7 = document.querySelector('.level7')

1.2 添加监听函数,点击事件,把自己的x去掉,获取到e事件,

level1.addEventlistener('click',(e)=>{
  e.currentTarget.classList.remove('x')
})

targetcurrentTarget的区别:

e.target,用户在操作的元素,也称为被cao的元素

e.currentTarget,程序员监听的元素

举例: div > span{文字},用户点击文字

e.target 就是 span

e.currentTarget 就是 div

1.3 延迟执行

let n = 1
level1.addEventlistener('click',(e)=>{
    setTimeout(()=>{
     e.currentTarget.classList.remove('x')
    },n*1000)  
})

1.4 间隔执行,记下点击的瞬间,默认监听函数不传参(或者传一个false)的话,就先执行最里面的level7,冒泡

let n = 1
level1.addEventlistener('click',(e)=>{
   const t =e.currentTarget
    setTimeout(()=>{
     t.classList.remove('x')
    },n*1000)
    n += 1
})
...
level7.addEventlistener('click',(e)=>{
   const t =e.currentTarget
    setTimeout(()=>{
     t.classList.remove('x')
    },n*1000)
    n += 1
})

1.5 改为捕获,监听函数后面加一个true,此时就先调用level1,捕获

let n = 1
level1.addEventlistener('click',(e)=>{
   const t =e.currentTarget
    setTimeout(()=>{
     t.classList.remove('x')
    },n*1000)
    n += 1
},true)
...
level7.addEventlistener('click',(e)=>{
   const t =e.currentTarget
    setTimeout(()=>{
     t.classList.remove('x')
    },n*1000)
    n += 1
},true)

1.6 实现冒泡和捕获的过程,执行时,先捕获,后冒泡

let n = 1
level1.addEventlistener('click',(e)=>{
   const t =e.currentTarget
    setTimeout(()=>{
     t.classList.remove('x')
    },n*1000)
    n += 1
},true)
level1.addEventlistener('click',(e)=>{
   const t =e.currentTarget
    setTimeout(()=>{
     t.classList.add('x')
    },n*1000)
    n += 1
})
...
level7.addEventlistener('click',(e)=>{
   const t =e.currentTarget
    setTimeout(()=>{
     t.classList.remove('x')
    },n*1000)
    n += 1
},true)
level7.addEventlistener('click',(e)=>{
   const t =e.currentTarget
    setTimeout(()=>{
     t.classList.add('x')
    },n*1000)
    n += 1
})
  1. 去掉重复代码,简化一下,
let n = 1
const removeX = (e)=>{
   const t =e.currentTarget
    setTimeout(()=>{
     t.classList.remove('x')
    },n*1000)
    n += 1
}
const addX = (e)=>{
   const t =e.currentTarget
    setTimeout(()=>{
     t.classList.add('x')
    },n*1000)
    n += 1
}

level1.addEventlistener('click',removeX,true)
level1.addEventlistener('click',addX)

e对象被传给所有监听函数,事件结束后,e对象就不存在了

3.1 特例: 只看level7这个div元素,也就是没有元素结点(这个div)与根结点(window)之间的路径传播了,那就是按代码行顺序谁先监听,谁先执行

level7.addEventlistener('click',()=>{console.log(1)})  //冒泡,因为没传参
level7.addEventlistener('click',()=>{console.log(2),true})  //捕获,传参了

点击元素时,先打印出1,再打印2

4.1 取消冒泡

捕获不可取消,但冒泡可以

e.stopPropagation()可中断冒泡,一般用于封装某些独立的组件。 浦肉赔给神(传播的意思)

4.2 如何阻止滚动

scroll event事件不可取消冒泡

Bubbles Yes //该事件是否冒泡 Cancelable No //开发者是否可以取消冒泡

要阻止滚动,可阻止wheel和touchstart的默认动作,滚动条在css里面::webkit-scrollbar{width:0 !important}或者使用over flow:hidden直接取消滚动条。 示例:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
  <div id=x> 很长
    <p>1</p>
    <p>2</p>
    <p>3</p>
    <p>4</p>
    <p>5</p>
    ...
    <p>100</p>
  </div>
</body>

</html>

先解决鼠标滚轮的滚动

x.addEventListener('wheel',(e)=>{
    e.preventDefault()
})

再解决滚动条隐藏,用css代码解决

::webkit-scrollbar{width:0 !important}

最后解决手机模式,禁用触屏事件

x.addEventListener('touchstart',(e)=>{
    e.preventDefault()
})