JS事件模型:冒泡/捕获

318 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

事件模型

W3C中定义事件的发生经历三个阶段:捕获阶段(capturing)—> 目标阶段(targetin)—> 冒泡阶段(bubbling

DOM事件流:同时支持两种事件模型:捕获型事件和冒泡型事件

事件冒泡还是事件捕获

事件传播是一种定义当发生事件时元素次序的方法。假如 <div> 元素内有一个 <p>,然后用户点击了这个 <p> 元素,应该首先处理哪个元素“click”事件?

在冒泡中,最内侧元素的事件会首先被处理,然后是更外侧的:首先处理 <p> 元素的点击事件,然后是 <div> 元素的点击事件。

在捕获中,最外侧元素的事件会首先被处理,然后是更内侧的:首先处理 <div> 元素的点击事件,然后是 <p> 元素的点击事件。

addEventListener() 方法中,你能够通过使用“useCapture”参数来规定传播类型:

addEventListener(event, function, useCapture);

默认值是 false,将使用冒泡传播,如果该值设置为true,则事件使用捕获传播。

  • true表示该元素在事件的“捕获阶段”(由外往内传递时)响应事件
  • false表示该元素在事件的“冒泡阶段”(由内向外传递时)响应事件

removeEventListener() 方法会删除已通过 addEventListener() 方法附加的事件处理程序:

element.removeEventListener(event, function);

冒泡

冒泡型事件:当你使用事件冒泡时,子级元素先触发,父级元素后触发

<head>
  <style>
    .parent{
      width: 200px;
      height: 200px;
      background-color: antiquewhite;
    }
    .child{
      width: 100px;height: 100px;
      margin: 30px;
      background-color: cadetblue;
    }
  </style>
</head>
<body>
  <div class="parent" onclick="parent()">
    父元素
    <div class="child" onclick="child()">子元素</div>
  </div>
  <script>
    function parent() {
      console.log('点击父元素')
    }
    function child() {
      console.log('点击子元素')
    }
  </script>
</body>

默认添加事件是冒泡模式

冒泡机制.png

阻止冒泡:在W3c中,使用stopPropagation()方法;在IE下设置cancelBubble = true

捕获

捕获型事件:当你使用事件捕获时,父级元素先触发,子级元素后触发

<head>
  <style>
    .parent{
      width: 200px;
      height: 200px;
      background-color: rgb(204, 245, 182);
    }
    .child{
      width: 100px;height: 100px;
      margin: 30px;
      background-color: cadetblue;
    }
  </style>
</head>
<body>
  <div class="parent" id="parent">
    父元素
    <div class="child" id="child">子元素</div>
  </div>
  <script>
    function parent() {
      console.log('点击父元素')
    }
    function child() {
      console.log('点击子元素')
    }
    // addEventListener第3个参数为true是捕获,false为冒泡
    document.getElementById("parent").addEventListener("click", parent, true);
    document.getElementById("child").addEventListener("click", child, true);
  </script>
</body>

捕获机制.png

阻止捕获:阻止事件的默认行为,例如click - <a>后的跳转。在W3c中,使用preventDefault()方法,在IE下设置window.event.returnValue = false

addEventListener的兼容

addEventListener兼容情况.png

IE 8、Opera 6.0 及其更早版本不支持 addEventListener()removeEventListener() 方法。不过,对于这些特殊的浏览器版本,您可以使用 attachEvent() 方法向元素添加事件处理程序,并由 detachEvent() 方法删除:

element.attachEvent(event, function);
element.detachEvent(event, function);

示例

跨浏览器解决方案:

var x = document.getElementById("myBtn");
if (x.addEventListener) {                    // 针对主流浏览器,除了 IE 8 及更正版本
    x.addEventListener("click", myFunction);
} else if (x.attachEvent) {                  // 针对 IE 8 及更早版本
    x.attachEvent("onclick", myFunction);
} 

事件的代理/委托

事件代理(Event Delegation),又称之为事件委托。是 JavaScript 中常用绑定事件的常用技巧。顾名思义,“事件代理”即是把原本需要绑定的事件委托给父元素,让父元素担当事件监听的职务。

  • 原理:事件委托是指将事件绑定目标元素的到父元素上,事件代理的原理是DOM元素的事件冒泡机制触发该事件

  • 优点:1. 可以减少事件注册,节省大量内存占用。2. 可以将事件应用于动态添加的子元素上,实现当新增子元素对象时无需再次对其绑定事件

  • 缺点: 使用不当会造成事件在不应该触发时触发

  • 常见使用场景:比如在table上代理所有tdclick事件就非常棒。或是ul中所有li上的事件可以通过ul来代理。

示例

<head>
  <style>
    li{
      width: 200px;
      margin: 10px 0;
      background-color: cornflowerblue;
    }
  </style>
</head>
<body>
  <ul id="ul">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
  </ul>
  <script>
    var ulEl = document.getElementById("ul")
    // 所有li的事件统一绑定在ul上
    ulEl.addEventListener('click', function(e){
      var target = e.target || e.srcElement;  // 取到实际触发的target
      if(!!target && target.nodeName.toUpperCase() === "LI"){
          console.log(target.innerHTML);
      }
    }, false);
    // 动态新增的li不需要额外绑定事件
    var li = document.createElement('li')
    li.innerText = '5'
    ulEl.appendChild(li)
  </script>
</body>

事件委托.png

事件“捕获”和“冒泡”执行顺序和事件的执行次数

  • 按照W3C标准的事件:首是进入捕获阶段,直到达到目标元素,再进入冒泡阶段

  • 事件执行次数(addEventListener):元素上绑定事件的个数

    • 注意1:前提是事件被确实触发
    • 注意2:事件绑定几次就算几个事件,即使类型和功能完全一样也不会“覆盖”
  • 事件执行顺序:判断的关键是否目标元素

    • 非目标元素:根据W3C的标准执行:捕获->目标元素->冒泡(不依据事件绑定顺序)
    • 目标元素:依据事件绑定顺序:先绑定的事件先执行(不依据捕获冒泡标准)
    • 最终顺序:父元素捕获->目标元素事件1->目标元素事件2->子元素捕获->子元素冒泡->父元素冒泡
    • 注意:子元素事件执行前提 事件确实“落”到子元素布局区域上,而不是简单的具有嵌套关系
<head>
  <style>
    .parent{
      width: 200px;
      height: 200px;
      background-color: rgb(204, 245, 182);
    }
    .current{
      width: 130px; height: 130px;
      margin: 15px;
      background-color: rgb(244, 191, 121);
    }
    .child{
      width: 70px;height: 70px;
      margin: 15px;
      background-color: cadetblue;
    }
  </style>
</head>
<body>
  <div class="parent" id="parent">
    父元素
    <div class="current" id="current">
      目标元素
      <div class="child" id="child">子元素</div>
    </div>
  </div>
  <script>
    document.getElementById("parent").addEventListener("click", function() {
      console.log('parent冒泡触发')
    }, false);
    document.getElementById("parent").addEventListener("click", function() {
      console.log('parent捕获触发')
    }, true);

    document.getElementById("current").addEventListener("click", function() {
      console.log('目标元素事件1')
    });
    document.getElementById("current").addEventListener("click", function() {
      console.log('目标元素事件2')
    });
    document.getElementById("current").addEventListener("click", function() {
      console.log('目标捕获触发')
    }, true);

    document.getElementById("child").addEventListener("click", function() {
      console.log('child冒泡触发')
    }, false);
    document.getElementById("child").addEventListener("click", function() {
      console.log('child捕获触发')
    }, true);
  </script>
</body>

点击下图红点处:

触发顺序.png

在一个DOM上同时绑定两个点击事件:一个用捕获,一个用冒泡。事件会执行几次,先执行冒泡还是捕获?

  • 该DOM上的事件如果被触发,会执行两次(执行次数等于绑定次数)
  • 如果该DOM是目标元素,则按事件绑定顺序执行,不区分冒泡/捕获
  • 如果该DOM是处于事件流中的非目标元素,则先执行捕获,后执行冒泡

W3C事件的 target 与 currentTarget 的区别?

  • target 只会出现在事件流的目标阶段
  • currentTarget 可能出现在事件流的任何阶段
  • 当事件流处在目标阶段时,二者的指向相同
  • 当事件流处于捕获或冒泡阶段时:currentTarget 指向当前事件活动的对象(一般为父级)

如何派发事件(dispatchEvent)?(如何进行事件广播?)

  • W3C: 使用 dispatchEvent 方法
  • IE: 使用 fireEvent 方法
var fireEvent = function(element, event){
    if (document.createEventObject){
        var mockEvent = document.createEventObject();
        return element.fireEvent('on' + event, mockEvent)
    }else{
        var mockEvent = document.createEvent('HTMLEvents');
        mockEvent.initEvent(event, true, true);
        return !element.dispatchEvent(mockEvent);
    }
}