DOM事件——事件模型与事件委托

248 阅读9分钟

一、DOM

文档对象模型DOM(Document Object Model)文档对象模型,是HTML和XML文档的编程接口它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。简言之,它会将web页面和脚本或程序语言连接起来。

简单来说就是把document作为根节点(DOM树),展开各种处理网页内容的方法和接口,对网页进行访问与操作(window.document)。能够使用如 JavaScript等脚本语言进行修改。

二、事件

事件是您在编程时系统内发生的动作或者发生的事情,系统响应事件后,如果需要,您可以某种方式对事件做出回应。例如:如果用户在网页上单击一个按钮,您可能想通过显示一个信息框来响应这个动作。

例如,click就是一个事件,可以在后面绑定函数执行功能。事件可以在文档(Document)结构的任何部分被触发,触发者可以是用户操作,也可以是浏览器本身。事件并不是只是在一处被触发和终止;他们在整个document中流动,拥有它们自己的生命周期。

三、 DOM 事件模型或 DOM 事件机制

DOM 事件模型或 DOM 事件机制也可以叫做DOM事件流,也可以说是事件传播顺序。

从一个简单的代码来举例:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
   <div id="box">Click</div>
</body>
</html>

思考:点击div时,事件的传播顺序是:

(1)div→body→HTML→document

还是

(2)document→HTML→body→div

呢?

在早期,IE走的是(1),网景走的是(2)

(1)从小到大,由内到外叫做事件冒泡,也就是IE的事件流

(2)从大到小,由外到内叫做事件捕获,也就是网景的事件流

在捕获阶段:

  • 浏览器检查元素的最外层祖先<html>,是否在捕获阶段中注册了一个onclick事件处理程序,如果是,则运行它。
  • 然后,它移动到<html>中单击元素的下一个祖先元素,并执行相同的操作,然后是单击元素再下一个祖先元素,依此类推,直到到达实际点击的元素。

在冒泡阶段,恰恰相反:

  • 浏览器检查实际点击的元素是否在冒泡阶段中注册了一个onclick事件处理程序,如果是,则运行它
  • 然后它移动到下一个直接的祖先元素,并做同样的事情,然后是下一个,等等,直到它到达<html>元素。

两家大佬互相倾轧,苦的是一大批搬砖的小工人,就请出了W3C来指定一个统一的标准——DOM Level 2 Events Specification(DOM2级事件模型)规定浏览器应该同时支持两种调用顺序,先走捕获,看有没有函数监听,有就执行,没有就跳过;然后再走冒泡,看有没有函数监听

DOM2级事件模型包括三个阶段:

**事件捕获阶段:**该阶段的主要作用是捕获截取事件

**处于目标阶段:**事件的目标接收到事件,但不会 作出相应。一般地,该阶段具有双重范围,即捕获阶段的结束,冒泡阶段的开始;

**事件冒泡阶段:**主要作用是将目标元素绑定事件执行的结果返回给浏览器,处理不同浏览器之间的差异,主要在该阶段完成(响应)

  • 疑问:那函数是不是要调用两次? 不是,可以自己选择把函数放在捕获阶段还是冒泡阶段

e.addEventLisenter('click',f2,true) // true按捕获方向执行函数

e.addEventLisenter('click',f2,false) // false按冒泡方向执行函数(这个也是默认值,就是最后不填布尔值,会默认是冒泡)

代码示例

一个彩虹圈圈,一层套一层,.x就是让所有的颜色变透明,接下来就是添加JS代码,让冒泡或捕获实现。根据调用的顺序把背景色依次亮起,通过事件监听依次取消.x

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');

let n = 1;
const removeX = (e) => {
    // e只存在事件点击的一瞬间,会在事件结束后自动消亡
  const t = e.currentTarget;
    //点击时获取到e,让它继续存在
  setTimeout(() => {
    t.classList.remove('x');
  }, n * 1000);
  n += 1;
};
level1.addEventListener('click', removeX);
level2.addEventListener('click', removeX);
level3.addEventListener('click', removeX);
level4.addEventListener('click', removeX);
level5.addEventListener('click', removeX);
level6.addEventListener('click', removeX);
level7.addEventListener('click', removeX);

**冒泡:**默认如果不传参或者是false的话就是冒泡,彩虹会从内到外依次亮起(从div7到div1)

**捕获:**加上true,颜色会从外到内依次亮起,(从div1到div7)

level1.addEventListener('click', removeX,true);
level2.addEventListener('click', removeX,true);
level3.addEventListener('click', removeX,true);
level4.addEventListener('click', removeX,true);
level5.addEventListener('click', removeX,true);
level6.addEventListener('click', removeX,true);
level7.addEventListener('click', removeX,true);

捕获和冒泡同时存在:

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', addX);
level2.addEventListener('click', removeX,true);
level2.addEventListener('click', addX);
level3.addEventListener('click', removeX,true);
level3.addEventListener('click', addX);
level4.addEventListener('click', removeX,true);
level4.addEventListener('click', addX);
level5.addEventListener('click', removeX,true);
level5.addEventListener('click', addX);
level6.addEventListener('click', removeX,true);
level6.addEventListener('click', addX);
level7.addEventListener('click', removeX,true);
level7.addEventListener('click', addX);

在捕获过程中删除x,就是添加颜色。然后在冒泡过程中添加x,就是删除颜色

先把颜色一层一层加进来,在把颜色一层一层删除,也就是先走捕获再走冒泡

四、DOM事件委托

当监听子元素时,事件冒泡会通过目标元素向上传递到父级,直到document,如果子元素不确定或者动态生成,可以通过监听父元素来取代监听子元素。

一个很好的例子是一系列列表项,如果你想让每个列表项被点击时弹出一条信息,您可以将click单击事件监听器设置在父元素<ul>上,这样事件就会从列表项冒泡到其父元素<ul>上。并且父元素能够通过target判断是哪个子元素,从而做相应的处理

  • 优点
    • 大量减少内存占用,减少事件注册 // 假如100个子元素触发同一个监听函数,可以将此放在父元素上面。
    • 新增元素实现动态绑定事件 // 用createElement创建的元素,可以将监听函数放到父元素上面。


  • 举例

    • div1有1个span,100个button(click1~100),监听所有button,只需要在div1绑定就可以了

      div1.addEventListener('click',(e)=>{
          const t = e.target
          if(t.tagName.toLowerCase() === 'button'){
              console.log('button 被点击了')
              console.log('button内容是'+t.textContent)
              console.log('button  data-id是'+t.dataset.id)   //获取data-id的值
          }
      })
      

      这时点击span或者其他地方是不会触发事件监听的,但是无论点击哪个button,都会触发

    • 监听目前不存在的元素的点击事件,可以先监听祖先

      setTimeout(()=>{
          const button = document.createElement('button')
          button.textContent = 'click 1'
          div1.appendChild(button)
      },1000)             //1秒后才有button
      
      div1.addEventListener('click',(e)=>{
          const t = e.target
          if(t.tagName.toLowerCase()==='button'){
              console.log('button 被点击了')
          }
      })            //在父元素绑定判断监听
      

五、阻止冒泡

阻止冒泡,也可以称作DOM事件流阻断,也就是在冒泡过程中,DOM事件流可以在经过的任意事件处理函数中终止----event.stopPropagation(),一般用于封装一些独立的组件

补充

不可取消冒泡

有些事件不可取消冒泡,MDN搜索scroll event,看到Bubbles(该事件是否冒泡)和Cancelable(开发者是否可以取消冒泡)

scroll事件不可取消冒泡,但可以阻止滚动!

阻止scroll默认动作没有用,依旧可以滚动,因为先有滚动才有滚动事件

要阻止滚动,可以阻止wheel(滚轮)和touchstart(手机触摸滚动)的默认动作

注意需要找准滚动条所在元素

但是滚动条还能用,可用CSS让滚动条width:0(直接搜索hidden scrol bar

CSS使用overflow:hidden可以直接取消滚动条,但此时JS依然可以修改scrollTop(滚动条存在与否是不能判断一个元素是否能滚动的)

滚动条出现在document

举例:

    • 如果在#x阻止冒泡是不能禁用滚动的,因为scroll不能阻止冒泡

    • 阻止滚动的默认事件也是没有用的,默认事件是一个事件触发后才有的动作,所以以下代码没有任何作用

    • x.addEventListener('scroll',(e)=>{
      	e.stopPropagation()
          e.preventDefault()
      })
      
    • 可以禁用滚轮(JS),隐藏滚动条(CSS),禁止触控(JS)

    • x.addEventListener('wheel',(e)=>{
          e.preventDefault()
      })   //禁用滚轮
      x.addEventListener('touchstart',(e)=>{
          e.preventDefault()
      })   //禁用触摸滚动
      
    • ::-webkit-scrollbar {
      	width: 0 !important
      }
      

自定义事件

button1.addEventListener('click',()=>{       //当button1被点击时,触发一个事件
	const event = new CustomEvent('huhu',{   //声明一个新的事件(事件名,{事件信息}
		detail:{name:'huhu',age:18},         //事件内容
		bubbles: true,                       //可以冒泡
		cancelable:false                     //不可取消冒泡
	})
	button1.dispatchEvent(event)             //触发event,只要点击button就会触发huhu事件
})
button1.addEventListener('huhu',(e)=>{
    console.log('huhu 事件触发了')
    console.log(e.detail)
})                                           //监听button1 
div1.addEventListener('huhu',(e)=>{
    console.log('huhu 事件触发了 div1监听成功')
    console.log(e.detail)
})                                           //监听div1
<div id="div1">
  <button id="button1">点击触发 huhu 事件</button>
</div>

封装一个事件委托

写一个函数,可以直接调用on事件

on('click','#div1','button',()=>{
	console.log('button 被点击了')
})       //直接调用on事件可以在div1上看button有没有被click

补充on事件

setTimeout(() => {
  const button = document.createElement('button');
  const span = document.createElement('span');
  span.textContent = 'click 1';
  button.appendChild(span);
  div1.appendChild(button);
}, 1000);

/***
* <div id='div1'>
*     <button>
*          <span>click 1</span>
*     </button>
* </div>
* 点击click 1 就是点击span(span并没有绑定事件监听),冒泡到button,触发click事件
*/


on('click', '#div1', 'button', () => {
  console.log('button 被点击了');
});

function on(eventType,element,selector,fn){
    if(!(element instanceof Element)){
        element = document.querySelector(element);
    }                                 //判断element是element还是选择器
    element.addEventListener(eventType,(e)=>{
        let t = e.target;
        while(!t.matches(selector)){      //matches用来判断一个元素是否满足一个选择器
        	if(element === t){      
                t = null;
                break;
            }                       //判断父级元素到div1停止
            t = t.parentNode;        //如果t!==element,就等于他的爸爸
        }
        t && fn.call(this, e, t);
    });
    return element;
}

target currentTarget

e.target 用户操作的元素

e.currentTarget 程序员监听的元素

thise.currentTarget

  • 举例

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

    e.target 就是 span

    e.currentTargetdiv

冒泡特例

只有一个div被监听(不考虑父子同时监听),那么用户点击的就是开发者监听的

这种情况就没有先捕获再冒泡的顺序,就是谁先监听谁先执行

参考资料

深入理解js Dom事件机制(一)—事件流

DOM事件机制详解

DOM事件介绍

DOM事件参考