事件冒泡和捕获、事件委托

前言

要理解DOM相关事件,我们先要理解“事件流”这个概念,事件流描述的是从页面中接收事件的顺序。
事件冒泡:事件开始由最具体的元素接收,然后逐级向上传播到较为不具体的节点或文档。
事件捕获:事件开始由不太具体的节点接收,然后逐级向下传播到最具体的节点。它与事件冒泡是个相反的过程。
DOM2 级事件规定的事件流包括三个阶段:事件捕获、目标阶段、事件冒泡。

    //事件有三个阶段:
    /*
     *
     * 1.事件捕获阶段  :从外向内
     * 2.事件目标阶段  :最开始选择的那个
     * 3.事件冒泡阶段  : 从里向外
     *
     * 为元素绑定事件
     * addEventListener("没有on的事件类型",事件处理函数,控制事件阶段的)
     * 事件触发的过程中,可能会出现事件冒泡的效果,为了阻止事件冒泡--->
     * window.event.cancelBubble=true;谷歌,IE8支持,火狐不支持
     * window.event就是一个对象,是IE中的标准
     * e.stopPropagation();阻止事件冒泡---->谷歌和火狐支持
     * window.event和e都是事件参数对象,一个是IE的标准,一个是火狐的标准
     * 事件参数e在IE8的浏览器中是不存在,此时用window.event来代替
     * addEventListener中第三个参数是控制事件阶段的
     * 事件的阶段有三个:
     * 通过e.eventPhase这个属性可以知道当前的事件是什么阶段你的
     * 如果这个属性的值是:
     * 1---->捕获阶段
     * 2---->目标阶段
     * 3---->冒泡
     * 一般默认都是冒泡阶段,很少用捕获阶段
     * 冒泡阶段:从里向外
     * 捕获阶段:从外向内

问题引入:

一个DOM元素绑定两个事件,一个冒泡,一个捕获,则事件会执行多少次,执行顺序如何。

这次不卖关思了,直接给你个答案。不理解你就继续往下看。

绑定在被点击元素的事件是按照代码顺序发生,其他元素通过冒泡或者捕获“感知”的事件,按照W3C的标准,先发生捕获事件,后发生冒泡事件。所有事件的顺序是:其他元素捕获阶段事件 -> 本元素代码顺序事件 -> 其他元素冒泡阶段事件

addEventListener参数

element.addEventListener(type, function, useCapture)
//【事件类型】,【事件处理程序】,【可选。布尔值,指定事件是否在捕获或冒泡阶段执行。true:事件句柄在捕获阶段执行;false:默认。事件句柄在冒泡阶段执行】

1.冒泡

冒泡是从下向上(子->父/内->外),DOM元素绑定的事件被触发时,此时该元素为目标元素,目标元素执行后,它的的祖元素绑定的事件会向上顺序执行。

addEventListener函数的第三个参数设置为false说明不为捕获事件,即为冒泡事件。

如下代码所示,四个嵌套的div:

  <div class="one">one
    <div class="two">two
      <div class="three">three
        <div class="four">four</div>
      </div>
    </div>
  </div>
  <script>
    let one = document.querySelector('.one')
    let two = document.querySelector('.two')
    let three = document.querySelector('.three')
    let four = document.querySelector('.four')

    one.addEventListener('click', e => {
      console.log('one')
    }, false)
    two.addEventListener('click', e => {
      console.log('two')
    }, false)
    three.addEventListener('click', e => {
      console.log('three')
    }, false)
    four.addEventListener('click', e => {
      console.log('four')
    }, false)
  </script>

事件的执行顺序是:
点击one元素,输出one;
点击two元素,输出two one;
点击three元素,输出 three two one;
点击four元素,输出 four three two one;

2.捕获

捕获则和冒泡相反,目标元素被触发后,会从目标元素的最顶层的祖先元素事件往下执行到目标元素为止。

将上面的代码第三个参数均改为true,事件为捕获阶段执行。

注意: 在事件处理程序中删除目标元素也能阻止事件冒泡,目标元素在文档中是事件冒泡的前提。

则执行结果如下:

点击one,输出one;
点击two,输出one two;
点击three,输出one two three;
点击four,输出one two three four;

3.当一个元素绑定两个事件,一个冒泡,一个捕获

首先,无论是冒泡事件还是捕获事件,元素都会先执行捕获阶段。
从上往下,如有捕获事件,则执行;一直向下到目标元素后,从目标元素开始向上执行冒泡元素,即第三个参数为true表示捕获阶段调用事件处理程序,如果是false则是冒泡阶段调用事件处理程序。(在向上执行过程中,已经执行过的捕获事件不再执行,只执行冒泡事件。)

one.addEventListener('click',function(){
alert('one');
},true);
two.addEventListener('click',function(){
alert('two');
},false);
three.addEventListener('click',function(){
alert('three');
},true);
four.addEventListener('click',function(){
alert('four');
},false);

此时点击four元素,four元素为目标元素,one为根元素祖先,从one开始向下判断执行。

分析:
one为捕获事件,输出one;
two为冒泡事件,忽略;
three为捕获时间,输出three;
four为目标元素,开始向上冒泡执行,输出four;(从此处分为两部分理解较容易。)
three为捕获已执行,忽略;
two为冒泡事件,输出two;
one为捕获已执行,忽略。

最终执行结果为: one three four two

例如,three作为目标元素,执行结果为:one three two(因为two是冒泡事件,在向下执行时没有执行到)。

执行次数:绑定了几个事件便执行几次。

如下代码,two元素绑定了两个不同事件,点击two都会执行这两个事件。而执行顺序有所差异。

    one.addEventListener('click', e => {
      console.log('one')
    }, true)
    two.addEventListener('click', e => {
      console.log('two, bubble')
    }, false)
    two.addEventListener('click', e => {
      console.log('two,capture')
    }, true)
    three.addEventListener('click', e => {
      console.log('three,bubble')
    }, true)
    four.addEventListener('click', e => {
      console.log('four')
    }, true)

1、如果two为目标元素,目标元素的同类型事件按顺序执行,而其他元素根据W3C的标准执行,即先捕获后冒泡。

点击two执行结果:one(因为是two的父元素支持捕获事件所以先执行) two bubble,two capture(顺序执行)

2、如果目标元素不是two,则two的同类型事件按先捕获后冒泡触发执行,也就是跟前面讨论的执行过程是一样的,只不过两个事件都绑定在同一个DOM元素上。

点击three执行结果:one ,two capture, three bubble ,two bubble

总结

所以,看到这里,你就应该明白了:

绑定在被点击元素的事件是按照代码顺序发生。
其他元素通过冒泡或者捕获“感知”的事件。
按照W3C的标准,先发生捕获事件,后发生冒泡事件。所有事件的顺序是:其他元素捕获阶段事件 -> 本元素代码顺序事件 -> 其他元素冒泡阶段事件

事件委托

事件委托,通俗的说就是将元素的事件委托给它的父级或者更外级的元素处理,它的实现机制就是事件冒泡。

这篇文章写的很好 - JavaScript 事件委托详解

基础实现

// 给父层元素绑定事件
document.getElementById('list').addEventListener('click', function (e) {
  // 兼容性处理
  var event = e || window.event;
  var target = event.target || event.srcElement;
  // 判断是否匹配目标元素
  if (target.nodeName.toLocaleLowerCase() === 'li') {
    console.log('the content is: ', target.innerHTML);
  }
});

完整代码

function eventDelegate (parentSelector, targetSelector, events, foo) {

  // 触发执行的函数
  function triFunction (e) {
    // 兼容性处理
    var event = e || window.event;

    // 获取到目标阶段指向的元素
    var target = event.target || event.srcElement;

    // 获取到代理事件的函数
    var currentTarget = event.currentTarget;

    // 处理 matches 的兼容性
    if (!Element.prototype.matches) {
      Element.prototype.matches =
        Element.prototype.matchesSelector ||
        Element.prototype.mozMatchesSelector ||
        Element.prototype.msMatchesSelector ||
        Element.prototype.oMatchesSelector ||
        Element.prototype.webkitMatchesSelector ||
        function(s) {
          var matches = (this.document || this.ownerDocument).querySelectorAll(s),
            i = matches.length;
          while (--i >= 0 && matches.item(i) !== this) {}
          return i > -1;            
        };
    }

    // 遍历外层并且匹配
    while (target !== currentTarget) {
      // 判断是否匹配到我们所需要的元素上
      if (target.matches(targetSelector)) {
        var sTarget = target;
        // 执行绑定的函数,注意 this
        foo.call(sTarget, Array.prototype.slice.call(arguments))
      }

      target = target.parentNode;
    }
  }

  // 如果有多个事件的话需要全部一一绑定事件
  events.split('.').forEach(function (evt) {
    // 多个父层元素的话也需要一一绑定
    Array.prototype.slice.call(document.querySelectorAll(parentSelector)).forEach(function ($p) {
      $p.addEventListener(evt, triFunction);
    });
  });
}

使用

eventDelegate('#list', 'li', 'click', function () { console.log(this); });

参考

掘金搜索

20k的前端是这样写事件委托的

一个DOM元素绑定多个事件时,先执行冒泡还是捕获

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值