DOM事件模型与事件委托

177 阅读4分钟

一、DOM事件模型

先看一段代码:

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

给3个 div 分别添加事件监听 fnYe、fnBa、fnEr

问1:点击谁

(1) 点击了文字,算不算点击了儿子?
(2) 点击了文字,算不算点击了爸爸?
(3) 点击了文字,算不算点击了爷爷?

3个都算

问2:调用顺序

点击文字,最先调用哪个函数 fnYe?fnBa?fnEr

都可以?浏览器的不同调用的顺序就不同:IE认为先调 fnEr ,网景认为先调fnYe

w3c制定标准

  • 规定浏览器同时支持两种调用顺序
  • 首先按 爷爷 >> 爸爸 >> 儿子 顺序看有没有事件监听函数 (从外到内的捕获)
  • 然后按 儿子 >> 爸爸 >> 爷爷 顺序看有没有事件监听函数 (从内到外的冒泡)
  • 有监听函数就调用,并提供监听内容,没有就跳过

问:那么函数都要执行两遍?

错,由 开发者 来决定,函数放在 捕获阶段 还是放在 冒泡阶段

DOM事件模型分为捕获和冒泡。

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

  • 捕获阶段:事件从window对象自上而下向目标节点传播的阶段;
  • 目标阶段:真正的目标节点正在处理事件的阶段;
  • 冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。
    示意图:

关于事件绑定api -------- x.addEventListener('click',fn,bool)

  • 如果 bool 不传值或者传 false,那么 fn 就只走冒泡
  • 如果 bool 传 true,那么 fn 就只走捕获
    示意图
    代码示例

二、事件委托

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

优点一:减少内存消耗,提高性能

场景1:如何给100个按钮添加点击事件

监听给这些个按钮的祖先,冒泡的时候判断 target 是否是这些个按钮中的一个

实现如下

优点二:动态绑定事件

场景2:如何监听目前不存在的元素的点击事件

监听元素的祖先,点击的时候看是不是想要监听的元素

实现如下

三、关于其他

1. target vs currentTarget

区别

  • target :用户操作的元素
  • currentTarget:程序员监听的元素

举例

  • div > span {文字},用户点击 span
  • e.target 是 span
  • e.currentTarget 是 div

特别的它1

背景

  • 只有一个 div 被监听时(不考虑父子同时被监听)
  • fn 分别在捕获阶段和冒泡阶段监听事件
  • 用户点击的元素就是开发者监听的元素
  • 先执行哪个函数?
  • 谁先监听谁就先执行

实现如下

2. 取消冒泡 ------ e.stopPropagation

但是捕获不可取消

事件特性

  • Bulles 表示是否冒泡
  • Cancelable 是否支持开发者取消冒泡 有些事件不可取消冒泡 如scroll事件

特别的它2 ------ 如何阻止滚动

  • 阻止 scroll 事件的默认动作没用,因为先有滚动才会有滚动事件

操作如下 示例

  • 找准滚动条所在的元素
  • 取消默认动作 ------- e.preventDefault()
  • 取消 滚轮 默认动作
  • 隐藏 滚动条
  • 移动端 取消触屏的默认动作
// js部分
// 取消滚轮默认动作
div1.addEventListener('wheel',(e=>{
  e.preventDefault()
}))
// 移动端 取消触屏的默认动作
div1.addEventListener('touchstart',(e=>{
  e.preventDefault()
}))

// css 部分
// 隐藏 滚动条   注意:这个方法不兼容IE,自定义滚动条的伪对象选择器::-webkit-scrollbar
::-webkit-scrollbar { width: 0 !important }

3. 自定义事件

  • 创建自定义事件 : const event = new CustomEvent ("对象名", {内容1,内容2、、、}
  • 触发事件 :.dispatchEvent(event)


例:给 buttom 自定义一个 chili 事件,并且默认可以冒泡,也不能阻止冒泡。效果

<div id=div1>
    <button id=button1>点击触发 chili 事件     
    </button>
  </div>
  
button1.addEventListener('click', () => {
  const event = new CustomEvent("chili", {
    "detail": {
      name: 'chili',
      age: 18
    },
    bubbles:true,
    cancelable:false
  })
  button1.dispatchEvent(event)
})

div1.addEventListener('chili', (e) => {
  console.log('chili')
  console.log(e)
  console.log(e.detail)
})

4. 封装事件委托

  • 写一个函数 on('click','#div1','button',fn)
  • 当用户点击 #div1 里面的 button 元素时,调用fn 函数


方法1:判断 target 是否匹配 button

  <div id=div1></div>
  
  setTimeout(()=>{
  const button = document.createElement('button')
  button.textContent = 'click'
  div1.appendChild(button)
},1000)

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

function on(eventType,element,selector,fn){
  if(!(element instanceof Element)){        
    element  = document.querySelector(element)
  }
  element.addEventListener(eventType,(e)=>{
    const t = e.target
    if(t.matches(selector)){        //判断元素是否被指定的选择器字符串所选择
      fn(e)
    }
  })
}

**方法2:判断 target的爸爸、爷爷、、、是否匹配 button

function on(eventType,element,selector,fn){
    element.addEventListener(eventType, e => {
      let t = e.target
      while (!t.matches(selector)) {
        if (element === t) {
          t = null
          break
        }
        t = t.parentNode
      }
     t && fn.call(t, e, t)
    })
  }