JavaScript 系列 -- 事件捕获、事件冒泡、事件委托

917 阅读6分钟

原文地址:

JS事件流模型

1. DOM 0级模型

也称为原始事件模型,这种方式较为简单且兼容所有浏览器,但是却将界面与逻辑耦合在一起,可维护性差。

实例

当点击idi3<div>时,控制台会依次输出2 1 0

<body>
    <div id="i1" onclick="console.log(0)">
        <div id="i2" onclick="console.log(1)">
            <div id="i3" onclick="console.log(2)"></div>
        </div>
    </div>
</body>

2. IE 事件模型

IE8及之前的版本是不支持捕获事件的,IE事件模型共有两个过程:

事件处理阶段target phase,事件到达目标元素, 触发目标元素的监听事件。

事件冒泡阶段bubbling phase事件从目标元素冒泡到document,依次执行经过的节点绑定的事件。

3. DOM 2级模型

DOM2事件模型是W3C制定的标准模型,支持捕获型事件和冒泡型事件,调用事件的处理阶段依次为捕获、目标、冒泡。

实例

当点击idi3<div>时,浏览器会依次弹出0 1 3 2addEventListener方法的第三个参数为声明绑定的事件为捕获型还是冒泡型,默认为false,也就是冒泡型。


<body>
    <div id="i1" onclick="console.log(0)">
        <div id="i2" onclick="console.log(1)">
            <div id="i3" onclick="console.log(2)"></div>
        </div>
    </div>
<script type="text/javascript">
    document.addEventListener('click',(e) => {
        console.log(0);
    },true) 
    document.getElementById("i1").addEventListener('click',(e) => {
        console.log(1);
    },true) 
    document.getElementById("i2").addEventListener('click',(e) => {
        console.log(2);
    })  
    document.getElementById("i3").addEventListener('click',(e) => {
        console.log(3);
    })     
</script>
</body>

document对象与i1节点绑定的是捕获型的监听事件,i2i3节点绑定的是冒泡型的事件,事件传递的顺序为:

window - document - html - body - i1 - i2 - i3 - i2 - i1 - body - html - document - window

windowi3的过程为捕获阶段,依次执行了过程中绑定的事件,本例中执行了alert(0)alert(1),然后到达目标阶段i3,执行i3绑定的事件alert(3),然后从i3window的阶段为冒泡阶段,执行了绑定的alert(2),执行顺序即为0 1 3 2

注意

绑定监听事件使用的区别

DOM0中直接绑定函数执行时,后定义的函数会覆盖前边绑定的函数,下面这个例子只执行alert(1)而不执行alert(0)click()是一个对象事件,点击即触发onclick()绑定的方法,onclick()是对象的属性,将其绑定函数后即为click()事件触发后执行的方法。

<body>
    <div id="i1"></div>
<script type="text/javascript">
    document.getElementById("i1").onclick = function(){
        alert(0); // 被覆盖
    }
    document.getElementById("i1").onclick = function(){
        alert(1); // 执行
    }
</script>
</body>

addEventListener可以为事件绑定多个函数,并且绑定时不需要加on,其还可以接收第三个参数useCapture来决定事件时绑定的捕获阶段还是冒泡阶段执行。

<script type="text/javascript">
    document.getElementById("i1").addEventListener('click',(e) => {
        alert(0); // 执行
    })
    document.getElementById("i1").addEventListener('click',(e) => {
        alert(1); // 执行
    })
</script>

attachEvent可以为事件绑定多个函数,绑定时需要加on,其只支持冒泡阶段执行,所以不存在第三个参数。

<script type="text/javascript">
    document.getElementById("i1").attachEvent('onclick',function(e){
        alert(0); // 执行
    })
    document.getElementById("i1").attachEvent('onclick',function(e){
        alert(1); // 执行
    })
</script>

事件捕获

事件捕获 是一种从外到内的传播方式,以click事件为例,其会从最外层根节向内传播到达点击的节点,为从最外层节点逐渐向内传播直到目标节点的方式。

image.png

【事件捕获过程先于事件冒泡过程】

事件冒泡及阻止

image.png

事件冒泡 是一种从内到外的传播方式,同样以click事件为例,事件最开始由点击的节点,然后逐渐向上传播直至最高层节点。

当一个元素接收到事件的时候,会把他接收到的事件传给自己的父级,一直到window,当然其传播的是事件,绑定的执行函数并不会传播,如果父级没有绑定事件函数,就算传递了事件,也不会有什么表现,但事件确实传递了。

事件冒泡的原因是事件源本身可能没有处理事件的能力,即处理事件的函数并未绑定在该事件源上。它本身并不能处理事件,所以需要将事件传播出去,从而能达到处理该事件的执行函数。

实例

当点击idi3<div>时,浏览器会依次弹出3 2 1,这就是事件冒泡,此正方形处于叶节点上,对其操作的事件会向上进行冒泡,直到根节点。

<body>
    <div id="i1">
        <div id="i2">
            <div id="i3"></div>
        </div>
    </div>
<script type="text/javascript">
    document.getElementById("i1").addEventListener('click',(e) => {
        alert(1);
    }) 
    document.getElementById("i2").addEventListener('click',(e) => {
        alert(2);
    })  
    document.getElementById("i3").addEventListener('click',(e) => {
        alert(3);
    })     
</script>
</body>

应用场景

例如我们有10<li>标签,每个标签有一个uid作为判断用户点击的区别,使用冒泡就不需要为每个<li>绑定点击事件,可以称为事件委托。

<body>
    <ul id="u1">
        <li uid="0">0</li>
        <li uid="1">1</li>
        <li uid="2">2</li>
        <li uid="3">3</li>
        <li uid="4">4</li>
        <li uid="5">5</li>
        <li uid="6">6</li>
        <li uid="7">7</li>
        <li uid="8">8</li>
        <li uid="9">9</li>
    </ul>
<script type="text/javascript">
    document.getElementById("u1").addEventListener('click',(e) => {
        alert(e.srcElement.getAttribute('uid'));
    })    
</script>
</body>

阻止冒泡

有时候我们并不希望事件冒泡而去执行上级节点绑定的事件,这时候就需要阻止事件的冒泡,w3c的方法是e.stopPropagation()IE则是使用 window.event.cancelBubble = true;

如果使用 vue 框架,则是@click.stop=""

注意

  • 不是所有的事件都能冒泡。以下事件不冒泡:blurfocusloadunload
  • 事件解决方案方式在不同浏览器,可能是有所区别的,有些不支持捕获型方案,多数浏览器默认冒泡型方案。
  • 阻止冒泡并不能阻止对象默认行为,例如submit按钮被点击后会提交表单数据,需使用e.preventDefault();阻止默认行为,IE则是window.event.returnValue = false;

事件委托

为什么要用事件委托:

比如我们有100个 li,每个 li 都有相同的 click 点击事件,可能我们会用 for 循环的方法,来遍历所有的li,然后给它们添加事件,这么做的话:会造成访问 dom 的次数很多,引起浏览器重绘与重排的次数也就很多,就会延长整个页面的交互就绪时间;

如果要用事件委托,就会将所有的操作放到 js 程序里面,与 dom 的操作就只需要交互一次,这样就能大大的减少与 dom 的交互次数,提高性能;

如果去监听每一个 li 的点击事件,就需要很多个变量来存 dom 对象,100 个 li 就需要 100 个变量对象,会造成很大的内存空间损耗。所以我们想着把利用事件冒泡由内到外的特点进行事件委托,只给所有 li 元素的父级元素 ul 添加点击事件,进行大大减少了内存空间的需求

事件委托的原理

image.png

给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发,这就是事件委托,委托它们父级代为执行事件

事件委托的实现

一个列表,要求实现功能是点击不同的 item 项,作出不同的操作

<ul>
  <li>hello 1</li>
  <li>hello 2</li>
  <li>hello 3</li>
  <li>hello 4</li>
</ul>

image.png

  • 没有使用事件委托
let arrLi = document.querySelectorAll('li')
for(let i = 0;i < aLi.length;i++){  
  aLi[i].addEventListener('click',function(e){
    console.log(e.target)  // li
    console.log(e.currentTarget)  // li
    console.og(e.target === e.currentTarget)  // true
  })
}
  • 使用事件委托
let ul = document.querySelectorAll('ul')[0]
ul.addEventListener('click',function(e){
  console.log(e.target)   //  被点击的li
  console.log(e.currentTarget)   // ul
  console.log(e.target === e.currentTarget)  // false
})
  • e.target 指向触发事件监听的对象。
  • e.currentTarget 指向添加监听事件的对象 点击 黄色区域橙色区域,分别打印:

image.png

所以给父级元素 ul 添加监听事件,监听事件对象 event 的 target 属性就可以拿到被点击的 li 节点

关于 e.target与e.currentTarget的区别 在这里


现在讲的都是document加载完成的现有dom节点下的操作,那么如果是新增的节点,新增的节点会有事件吗?就好比,一个新员工来了,他能收到快递吗?

需求场景:1. 移入li,li变红,移出li,li变白,给节点添加事件实现;2. 点击按钮,可以向ul中添加一个li子节点

<ul id="ul1">
    <li>111</li>
    <li>222</li>
    <li>333</li>
    <li>444</li>
</ul>
<input type="button" name="" id="btn" value="添加" />
  • 没有使用事件委托
window.onload = function () {
  var oBtn = document.getElementById("btn");
  var oUl = document.getElementById("ul1");
  var aLi = oUl.getElementsByTagName("li");
  var num = 4;

  function mHover() {
    for (var i = 0; i < aLi.length; i++) {
      aLi[i].onmouseover = function () {
        this.style.background = "red";
      };
      aLi[i].onmouseout = function () {
        this.style.background = "#fff";
      };
    }
  }
  mHover(); // 给当前的每个节点添加事件,在没有点击新增按钮的前提下还有
  oBtn.onclick = function () { // 添加新节点
    num++;
    var oLi = document.createElement("li");
    oLi.innerHTML = 111 * num;
    oUl.appendChild(oLi);
    mHover(); // 再次给当前的每个节点添加事件
  };
};

缺点:无疑是又增加了一个 dom 操作,在优化性能方面是不可取的

  • 使用事件委托
window.onload = function () {
  var oBtn = document.getElementById("btn");
  var oUl = document.getElementById("ul1");
  var aLi = oUl.getElementsByTagName("li");
  var num = 4;

  // 事件委托,后面添加的子元素也有事件
  oUl.onmouseover = function (ev) {
    var ev = ev || window.event;
    var target = ev.target || ev.srcElement;
    if (target.nodeName.toLowerCase() == "li") {
      target.style.background = "red";
    }
  };
  oUl.onmouseout = function (ev) {
    var ev = ev || window.event;
    var target = ev.target || ev.srcElement;
    if (target.nodeName.toLowerCase() == "li") {
      target.style.background = "#fff";
    }
  };

  // 添加新节点,无需给新节点添加事件
  oBtn.onclick = function () {
    num++;
    var oLi = document.createElement("li");
    oLi.innerHTML = 111 * num;
    oUl.appendChild(oLi);
  };
};