DOM 事件模型

本文介绍了DOM事件机制,包括捕获与冒泡阶段,详细解释了如何取消冒泡以及target和currentTarget的区别。此外,重点阐述了事件委托的概念,通过应用场景展示了事件委托如何节省内存并监听动态元素。最后讨论了事件委托与事件冒泡和捕获的关系。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、引入:

对于以下代码

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

给这三个盒子都添加事件监听

  • 点击文字,算不算点击儿子?
  • 点击文字,算不算点击爸爸?
  • 点击文字,算不算点击爷爷?
    都算
    那么他们的顺序是什么呢。
    在捕获阶段,爷爷->爸爸->儿子
    在冒泡阶段,儿子->爸爸->爷爷

二、DOM 事件机制

捕获与冒泡

上述过程发生了什么,为什么点击文字,就算把三个div全部点击了呢。

DOM 事件机制主要有 2 个阶段,分别是:捕获阶段和冒泡阶段
先执行捕获阶段,再执行冒泡阶段
比如刚才说的点击事件,执行的顺序就是

  • 先是捕获阶段:爷爷->爸爸->儿子
  • 接着是冒泡阶段:儿子->爸爸->爷爷

只要添加了时间监听,就会按照上面的顺序执行。
在这里插入图片描述
事件监听函数是addEventListener('click', fn, bool)
如果不穿bool的值或者bool值为false,fn函数就只在冒泡阶段执行
如果bool的值是true,那么fn函数就在捕获阶段执行。
在这里插入图片描述

取消冒泡

捕获不可以取消,但是冒泡可以取消,e.propagation()就可
但是有一些事件不可以取消冒泡,比如 scroll 事件,具体可以在 MDN 上查询

target 和 currentTarget 的区别

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

<div><span>内容</span></div>

加入程序员监听的是div,用户能看见的就是内容,用户点击内容,
事件e.target就是span,而e.currentTarget就是div

三、事件委托

先来一个应用场景:我们要给 100 个按钮添加点击事件,怎么办?最笨的办法:直接给 100 个按钮都 addEventListener有了事件委托后:监听这 100 个按钮的爸爸,等冒泡的时候,判断 target 是不是这 100 个按钮中的一个

场景二:我们要监听目前不存在的元素的点击事件,咋办?有了事件委托:监听祖先,等到冒泡时,判断点击的元素是不是我想要监听的元素

所以使用事件委托的好处就是

  • 可以省掉监听数,从而节省内存(用少量的监听器,监听多个元素)
  • 可以监听动态元素

例子
需求:监听所有的 li 标签,如果用户点击 li 标签,就 console.log(‘用户点击了 li 标签’)

// 监听父元素 ul#test
test.addEventListener('click', (e)=> {
  //通过浏览器传进来的e参数,找到当前点击元素
  const t = e.target
  // 判断当前元素是不是Li标签
  if(t.matches('li') {
    console.log('用户点击了li')
  }
})

思路很简单:
利用事件委托

  1. 监听父元素
  2. 然后根据冒泡和捕获,看点击的元素是否是li

就实现了事件委托。

封装成函数

on("click", "#test", "li", () => {
  console.log("用户点击了li");
});

function on(eventType, element, selector, fn) {
  // 先判断是不是element,
  //如果传进来的是选择器,不是element本身,就先变成element,
  // 因为只有element才能监听事件`在这里插入代码片`
  if (!(element instanceof Element)) {
    element = element.querySelectorAll(element);
  }
  element.addEventListener(eventType, (e) => {
    let target = e.target;
    if (target.matches(selector)) {
      fn(e);
    }
  });
}

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

<ul id="test">
  <li>
    <p>
      <span>1</span>
    </p>
  </li>
  <li>
    <p>
      <span>2</span>
    </p>
  </li>
  <li>
    <p>
      <span>3</span>
    </p>
  </li>
  <li>
    <p>
      <span>4</span>
    </p>
  </li>
</ul>

我们需要做的就是:
递归地向上多找几层父节点,直到找到 li 标签,
同时还必须限定,寻找的范围不能超过 element,
拿上面的例子来说,不可以越过 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);
  });
}

事件委托和事件冒泡,捕获有关系吗

我觉得这是一个很好的问题,很少有人思考这种问题。
刚才在搜了很多博客,就第一个例子而言,事件委托是依赖冒泡事件的,如果不依赖事件冒泡,也是必须依赖事件捕获的,不然不可能有事件委托。

做一个说明,e.target是用户所操作的元素,所以按照第一个例子,一个div中一样一百个子元素。
e.target获取到的是被点击的子元素。我们是在div上添加了点击事件,所以是通过冒泡上去触发的这个点击事件。所以事件委托肯定和事件冒泡和事件捕获其中之一有关系。
事件委托的本质是解决上面两个场景的问题,不要忘了场景

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值