JavaScript事件传递机制Event Propagation

event propagation事件冒泡

element.addEventListener('click', listener, useCapture); useCapture 默认是false, 表示使用bubble

什么是事件传播

一个完整的js事件流是从window开始,最后回到window的过程,如下图所示:

event_phase.png

在DOM事件标准中,定义了事件传播的3个阶段

  1. capturing phase 捕获阶段,事件从dom tree的上方向下方传递
  2. target phase 目标阶段,事件到达目标元素
  3. bubbling phase冒泡阶段,事件从该元素向上传递。即触发子元素中注册的事件,再触发父元素中注册的事件。

事件冒泡

在一个对象上触发某类事件(比如单击onclick事件),如果此对象定义了此事件的处理程序,那么此事件就会调用这个处理程序,如果没有定义此事件处理程序或者事件返回true(因此可以通过return false来阻止,但比较暴力),那么这个事件会向这个对象的父级对象传播,从里到外,直至它被处理(父级对象所有同类事件都将被激活),或者它到达了对象层次的最顶层,即document对象(有些浏览器是window)。

微软提出了名为事件冒泡(event bubbling)的事件流。事件冒泡可以形象地比喻为把一颗石头投入水中,泡泡会一直从水底冒出水面。也就是说,事件会从最内层的元素开始发生,一直向上传播,直到document对象。
因此在事件冒泡的概念下在div元素上发生click事件的顺序应该是div -> body -> html -> document

事件捕获

网景提出另一种事件流名为事件捕获(event capturing)。与事件冒泡相反,事件会从最外层开始发生,直到最具体的元素。
因此在事件捕获的概念下在div元素上发生click事件的顺序应该是document -> html -> body -> div -> div

w3c 采用折中的方式,制定了统一的标准——先捕获再冒泡

addEventListener第三个参数默认值是false,表示在事件冒泡阶段调用事件处理函数;如果参数为true,则表示在事件捕获阶段调用处理函数。

外层红色框,中间黄色框,最里面蓝色框


Screen Shot 2020-01-14 at 12.18.05 AM.png

测试事件冒泡-点击蓝色

s1 = document.getElementById('s1')
s2 = document.getElementById('s2')
s3 = document.getElementById('s3')

s1.addEventListener("click",function(e){
    console.log("红 冒泡事件");//从底层往上
},false);//第三个参数默认值是false,表示在事件冒泡阶段调用事件处理函数;如果参数为true,则表示在事件捕获阶段调用处理函数。
s2.addEventListener("click",function(e){
    console.log("黄 冒泡事件");
},false);
s3.addEventListener("click",function(e){
    console.log("蓝 冒泡事件");
},false);

结果为:

蓝 冒泡事件

黄 冒泡事件

红 冒泡事件

测试事件捕获-点击蓝色

s1.addEventListener("click",function(e){
    console.log("红 捕获事件");
},true);

s2.addEventListener("click",function(e){
    console.log("黄 捕获事件");

},true);
s3.addEventListener("click",function(e){
    console.log("蓝 捕获事件");
},true);

结果为:
红 冒泡事件
黄 冒泡事件
蓝 冒泡事件

事件捕获与事件冒泡同时存在

事件捕获过程中,先捕获后冒泡
事件到达目标节点,先注册先执行

  • 这里记被点击的DOM节点为target节点,document 往 target节点,捕获前进,遇到注册的捕获事件立即触发执行
  • 到达target节点,触发事件
  • 对于target节点上,是先捕获还是先冒泡,根据捕获事件和冒泡事件的注册顺序,先注册先执行
  • target节点 往 document 方向,冒泡前进,遇到注册的冒泡事件立即触发
s1.addEventListener("click",function(e){
    console.log("红 冒泡事件");
},false);
s2.addEventListener("click",function(e){
    console.log("黄 冒泡事件");
},false);
s3.addEventListener("click",function(e){
    console.log("蓝 冒泡事件");
},false);

s1.addEventListener("click",function(e){
    console.log("红 捕获事件");
},true);

s2.addEventListener("click",function(e){
    console.log("黄 捕获事件");

},true);
s3.addEventListener("click",function(e){
    console.log("蓝 捕获事件");
},true);

红 捕获事件

黄 捕获事件

==蓝 冒泡事件==

==蓝 捕获事件==

黄 冒泡事件

红 冒泡事件

事件监听的方法

在不使用任何框架的情况下,我们在js中通过addEventListener方法给Dom添加事件监听。这个方法有三个参数可以传递addEventListener(event,fn,useCapture)。event是事件类型click,focus,blur等;fn是事件触发时将执行的函数方法(function);第三个参数可以不传,默认是false,这个参数控制是否捕获触发。所以我们只传两个参数时,这个事件是冒泡传递触发的,当第三个参数存在且为true时,事件是捕获传递触发的。

阻止事件冒泡和阻止默认事件的方法

event.stopPropagation()方法

这是阻止事件的冒泡方法,不让事件向documen上蔓延,但是默认事件任然会执行,当你调用这个方法的时候,如果点击一个连接,这个连接仍然会被打开

event.preventDefault()方法

这是阻止默认事件的方法,调用此方法是,连接不会被打开,但是会发生冒泡,冒泡会传递到上一层的父元素
preventDefault它是事件对象(Event)的一个方法,作用是取消一个目标元素的默认行为。既然是说默认行为,当然是元素必须有默认行为才能被取消,如果元素本身就没有默认行为,调用当然就无效了。什么元素有默认行为呢?如链接,提交按钮等。当Event 对象的 cancelable为false时,表示没有默认行为,这时即使有默认行为,调用preventDefault也是不会起作用的。

return false (jQuery)

这个方法比较暴力,他会同事阻止事件冒泡也会阻止默认事件;写上此代码,连接不会被打开,事件也不会传递到上一层的父元素;可以理解为return false就等于同时调用了event.stopPropagation()event.preventDefault()

jQuery源码:

if (ret===false){
  event.preventDefault();
  event.stopPropagation();
}

示例:

event_propagation.png

代码如下:


.box1 {
  height: 200px;
  width: 600px;
  margin: 0 auto;
  background: yellow;
}

.box1 a {
  display: block;
  height: 50%;
  width: 50%;
  background: red;
}

// 不阻止事件冒泡和默认事件,点击红色方块,事件从最里层的红色方块,依次冒泡到外层Box,依次弹出A Link, Box1, 随后页面跳转

window.onload = function () {
  let box1 = document.getElementsByClassName('box1')[0];
  let link = document.getElementsByTagName('a')[0];

  link.addEventListener('click', (event) => {
    alert('A Link')
  });

  box1.addEventListener('click', (event) => {
    alert('Box1');
  });
}

// 阻止事件冒泡,点击红色方块,弹出A Link, 随后页面跳转

window.onload = function () {
  let box1 = document.getElementsByClassName('box1')[0];
  let link = document.getElementsByTagName('a')[0];

  link.addEventListener('click', (event) => {
    event.stopPropagation();
    alert('A Link')
  });

  box1.addEventListener('click', (event) => {
    alert('Box1');
  });
}

//阻止事件冒泡和默认事件,点击红色方块,弹出A Link,页面不会跳转

window.onload = function () {
  let box1 = document.getElementsByClassName('box1')[0];
  let link = document.getElementsByTagName('a')[0];

  link.addEventListener('click', (event) => {
    event.stopPropagation();
    event.preventDefault();
    
    alert('A Link');
    
   // 也可以用return false; 代替 event.stopPropagation();event.preventDefault();
  });

  box1.addEventListener('click', (event) => {
    alert('Box1');
  });
}

在W3C Document Object Model Events Specification1.3版本中提到过:

The EventListener interface is the primary method for handling events. Users implement the EventListener interface
and register their listener on an EventTarget using the AddEventListener method. The users should also remove their
EventListener from its EventTarget after they have completed using the listener.

事件处理程序的返回值只对通过属性注册的处理程序才有意义,如果我们未通过addEventListener()函数来绑定事件的话,若要禁止默认事件,用的就是return false; 但如果要用addEventListener()或者attachEvent()来绑定,就要用preventDefault()方法或者设置事件对象的returnValue属性。

HTML5 Section 6.1.5.1 of the HTML Spec规范定义如下:

Otherwise If return value is a WebIDL boolean false value, then cancel the event.

而H5规范中为什么要OtherWise来强调return false,因为规范中有指出在mouseover等几种特殊事件情况下,return false;并不一定能终止事件。所以,在实际使用中,我们需要尽量避免通过return false;的方式来取消事件的默认行为。

jQuery中事件代理中,return false可能存在的问题

例如鼠标被按下后,mousedown事件被触发。
事件先从document->ancestor element->...->parent->event.target(在此元素上按下的鼠标)->parent->...->ancestor element->document.
事件走了一个循环,从documet到event.target再回到document,从event.target到document的过程叫做冒泡。

event.stopPropagation(); // 事件停止冒泡到,即不让事件再向上传递到document,但是此事件的默认行为仍然被执行,如点击一个链接,调用了event.stopPropagation(),链接仍然会被打开。

event.preventDefault(); // 取消了事件的默认行为,如点击一个链接,链接不会被打开,但是此事件仍然会传递给更上一层的先辈元素。

在事件处理函数中使用 return false; 相当于同时调用了event.stopPropagation()和event.preventDefault(),事件的默认行为不会被执行,事件也不会冒泡向上传递。

此时在jQuery中,return false;就不是简单的覆盖面和规范的问题了。在jQuery事件处理函数中调用return false;相当于同时调用了preventDefault和stopPropagation方法,这会导致当前元素的事件无法向上冒泡,在事件代理模式下,会导致问题。

比如,我有一个div容器,里面是 几个a标签,它们的href里分别存储了url地址,这个url被用来动态的载入到下面的div#content中,这里为了简单演示,就只把url字符串写入到div#content中:

content1 content2
我会根据点击链接的url不同而改变的
// 为container下的所有a标签绑定click事件处理函数
$("#container").click(function (e) {
   if (e.target.nodeName == "A") {
        $("#content").html(e.target.href);
    }
});
// 再为a标签绑定click事件处理函数,阻止默认事件
$("#container a").click(function () {
  return false;
});

上面的代码运行后,虽然阻止了a标签的点击默认行为,但同时停止了冒泡事件,导致其外层的父元素无法检测到click事件,所以jQuery中需要明白return false;和event.preventDefault()二者的区别。

即尽量不要用return false;来阻止event的默认行为。

IE下阻止默认事件:window.event.returnValue=false

function stopDefault( e ) { 
   if ( e && e.preventDefault ){ 
    e.preventDefault();  //支持DOM标准的浏览器
   } else { 
    window.event.returnValue = false;  //IE
   } 
}

应用:事件委托(也叫事件代理)

比如我想点击ul标签里面的li获取它的值,有点人就会遍历去给每个li加一个事件监听
其实我们可以在li的父级加一个事件监听,这就相当于把事件监听委托给了ul。
我们点击li的时候,事件冒泡到ul,被注册在ul的事件代理给捕获到,实现了事件委托机制。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
ul = document.getElementById('ur')

ul.addEventListener("click",function(e){
    console.log(e.target.innerText);
},false);

注意:addEventListener()必须用removeEventListener()解除

应用场景举例

我们在使用中多数情况下只使用冒泡监听。例如一条购物车信息,在这条信息中,右下角有一个删除按钮。点击这条消息可查看详情,点击删除按钮可将此商品移除。我们会分别给信息的div和删除button添加一个冒泡的click事件监听。如果不做阻止传递,点击删除button后,会显示商品详情。显然这不是我们想看到的。这时我们给button一个阻止事件传递的功能,点击删除按钮后,事件就会结束,就不再显示商品详情。

你可能感兴趣的:(JavaScript事件传递机制Event Propagation)