简术 DOM事件模型(机制) 与 DOM事件委托

137 阅读5分钟

一、DOM事件模型(机制)

事件捕获和事件冒泡

  • 从外向内找监听函数,叫事件捕获
  • 从内向外找监听函数,叫事件冒泡
引入点击事件
<div class='爷爷'>
   <div class='爸爸'>
       <div class='儿子'>文字</div>
   </div>
</div>
以上代码即.爷爷>.爸爸>.儿子
给三个div分别添加事件监听 fnYe / fnBa / fnEr  点击谁监听谁

问:调用监听函数顺序是什么呢

  • 事件捕获:fnYe > fnBa > fnEr 也就是从外到内去调用

  • 事件冒泡:fnEr > fnBa > fnYe 也就是从内到外去调用

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

  1. 捕获阶段:当用户点击按钮,浏览器会从 window 从上向下遍历至用户点击的按钮,逐个触发事件处理函数。(示例代码中简化为: 爷爷->爸爸->儿子);

  2. 目标阶段:真正的目标节点正在处理事件的阶段;(示例代码中: 文字)

  3. 冒泡阶段:浏览器从用户点击的按钮从下往上遍历至 window,逐个触发事件处理函数。(示例代码中简化为: 儿子->爸爸->爷爷)。

示意图

image.png

如何指定走捕获还是冒泡

事件绑定 API addEventListener

baba.addEventListener('click',fn,bool)//w3c制定

如果bool不传或为falsy:

就让fn走冒泡,即当浏览器在冒泡阶段发现baba有fn监听函数,就会调用fn,并提供时间信息。

如果bool为true:

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

image.png 代码示例 image.png

target 与 currentTarget的区别

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

例子:

<div>
  <span>文字</span>
</div>
  • e.target 就是 span ,用户操作的元素。
  • e.currentTarget 就是 div , 程序员监听的元素。

取消冒泡

e.stopPropagation() 可打断冒泡,浏览器不再向上走

一般用于封装某些独立组件

注意:捕获不可以取消但是冒泡可以(有些事件也不能够取消冒泡,例如: scroll 滚动条)

不可阻止默认动作

有些事件不能阻止默认动作

  • MDN搜索scroll event,看到Bubbles和 Cancelable
  • Bubbles的意思是该事件是否冒泡,所有冒泡都可取消
  • Cancelable的意思是开发者是否可以阻止默认事件
  • Cancelable与冒泡无关
  • 推荐看MDN英文版,中文版内容不全

小结

  1. target和currentTarget

    • 一个是用户点击的,一个是开发者监听的
  2. 取消冒泡

    • e.stopPropagation()
  3. 事件的特性

    • Bubbles表示是否冒泡
    • Cancelable表示是否支持开发者取消冒
    • 如scroll不支持取消冒泡
  4. 如何禁用滚轮滚动

    • 取消特定元素的wheel和touchstart的默认动作 image.png
  5. 取消滚动条

    • 可用CSS让滚动条width: 0 (::-webkit-scrollbar { width: 0 !important })
    • 使用overflow: hidden可以直接取消滚动条
  6. 取消触屏事件

    • 阻止触屏的touch事件

image.png

二、DOM事件委托

什么是事件委托

事件委托是一种提高程序性能,降低内存空间的技术手段,它利用了事件冒泡的特性,只需要在某个祖先元素上注册一个事件,就能管理其所有后代元素上同一类型的事件。

举个通俗的例子: 比如一个宿舍的同学同时快递到了,一种方法就是他们一个个去领取,还有一种方法就是把这件事情委托给宿舍长,让一个人出去拿好所有快递,然后再根据收件人一 一分发给每个宿舍同学; 在这里,取快递就是一个事件,每个同学指的是需要响应事件的 DOM 元素,而出去统一领取快递的宿舍长就是代理的元素,所以真正绑定事件的是这个元素,按照收件人分发快递的过程就是在事件执行中,需要判断当前响应的事件应该匹配到被代理元素中的哪一个或者哪几个

常见应用场景

场景一

  • 你要给100个按钮添加点击事件,咋办?
  • 答:监听这100个按钮的祖先,等冒泡的时候判断target是不是这100个按钮中的一个
    代码示例 image.png

场景二

  • 你要监听目前不存在的元素的点击事件,咋办?
  • 答:监听祖先,等点击的时候看看是不是我想要监听的元素即可 image.png

优点

  • 省监听数(内存)
  • 可以监听动态元素

补充

  • dataset可以获取以data开头的属性的值

封装一个事件委托

  • 基于场景二,我们可以封装一个事件委托函数
  • 实现这样一个函数 on(click,#testDiv',"li',fn)
  • 当用户点击 #testDiv 里的 li 元素时,调用 fn 函数
on("click", "#test", "li", () => {
  console.log("用户点击了li");
});
function on(eventType, parentElement, selector, fn) {
  // 先判断是不是element,
  //如果传进来的是选择器,不是element本身,就先变成element,
  // 因为只有element才能监听事件
  if (!(parentElement instanceof Element)) {
    parentElement = parentElement.querySelectorAll(parentElement);
  }
  parentElement.addEventListener(eventType, (e) => {
    let target = e.target; 
    if (target.matches(selector)) {      //判断 target 是否匹配li
      fn(e);
    }
  });
}

但是以上这种实现有一个小问题,那就是如果被点击元素有多个父元素怎么办?

  • 我们需要做的就是: 递归地向上多找几层父节点,直到找到 li 标签,
  • 同时还必须限定,寻找的范围不能超过 parentElement
  • 拿上面的例子来说,不可以越过 ul 标签,去找 body 标签
on("click", "#test", "li", () => {
  console.log("用户点击了li");
});
function on(eventType, element, selector, fn) {
  if (!(element instanceof Element)) {
    element = document.querySelectorAll(element);
  }
  element.addEventListener(eventType, (e) => {
    let target = e.target;
    // 如果匹配到了selector就跳出循环
    while (!target.matches(selector)) {
      if (target === element) {
        //已经找到了父元素,说明还没找到,就设置为null
        target = null;
        break;
      }
      target = target.parentNode;
    }
    // 找到了target, 就调用函数
    target && fn.call(target, e);
  });
}