事件的冒泡和捕获

DOM事件发生后,会在当前节点和父节点之间传播(propagation)。

事件传播按照传播顺序分为三个阶段。对应Event.prototype.eventPhase的三个状态:

  const phases = {
    1: 'capture', //捕获
    2: 'target',   // 目标
    3: 'bubble'   // 冒泡
  }

一. 事件传播阶段

1. 捕获阶段

事件按照window->document(window.document)->html(window.documentElement)

->body(document.body)->父节点->当前节点(target)顺序传递

对应监听函数如下:

element.addEventListener = function(type, function(e) {
   // TODO
}, true);  

2. 目标阶段

当触发对应的节点就是当前监听的节点。

不管addEventListener的第三个参数是true还是false,都会触发执行。

3. 冒泡阶段

事件按照target->父节点-> body->html->document->window顺序传递.

html标签的on[event] 属性和dom对象的on[event]方法都是监听的冒泡阶段的事件。

示例:

<body>
  <div id="container">
    <div id="root">
      <button id="btn">ClickMe</button>
    </div>    
  </div>

<script>  
  const phases = {
    1: 'capture',
    2: 'target',
    3: 'bubble'
  }
  container.addEventListener('click', function(e) {
    console.log('container->',phases[e.eventPhase]);
  },true);
  container.addEventListener('click', function(e) {
    console.log('container->',phases[e.eventPhase]);
  },false);
  root.addEventListener('click', function(e) {
    console.log('root->',phases[e.eventPhase]);
  },true);
  root.addEventListener('click', function(e) {
    console.log('root->',phases[e.eventPhase]);
  },false);
  btn.addEventListener('click', function(e) {
    console.log('btn->',phases[e.eventPhase]);
  }, true)
  btn.addEventListener('click', function(e) {
    console.log('btn->',phases[e.eventPhase]);
  }, false)
</script> 

运行结果:

// 当单击button时
container->capture
root->capture
btn->target
btn->target
root->bubble
container->capture

// 当单击root时
container->capture
root->target
root->target
container->bubble

// 当单击container时
container->target
container->target

二. 事件传播的相关方法

1. event.stopPropagation()

停止向上/向下传播;但是还可以监听同一事件。

// 事件传播到 element 元素后,就不再向下传播了
element.addEventListener('click', function (event) {
  event.stopPropagation();
}, true);

// 事件冒泡到 element 元素后,就不再向上冒泡了
element.addEventListener('click', function (event) {
  event.stopPropagation();
}, false);

示例:

  <div id="container">
    <div id="root">
      <button id="btn">ClickMe</button>
    </div>    
  </div>
<script>  
  const phases = {
    1: 'capture',
    2: 'target',
    3: 'bubble'
  }
  container.addEventListener('click', function(e) {
    console.log('container->',phases[e.eventPhase]);
  },true);
  container.addEventListener('click', function(e) {
    console.log('container->',phases[e.eventPhase]);
  },false);
  root.addEventListener('click', function(e) {
    event.stopPropagation(); // 停止传播
    console.log('root->',phases[e.eventPhase]);
  },true);
  root.addEventListener('click', function(e) {
    console.log('root->',phases[e.eventPhase]);
  },false);
  btn.addEventListener('click', function(e) {
    console.log('btn->',phases[e.eventPhase]);
  }, true)
  btn.addEventListener('click', function(e) {
    console.log('btn->',phases[e.eventPhase]);
  }, false)
</script> 
View Code

运行结果如下:

// 单击btn
container->capture
root->capture

// 单击root
container->capture
root->target
root->target  // 不会拦截目标节点的事件多次触发

2. event.stopImmediatePropagation()

停止事件传播,并且停止事件再次监听同样的事件

element.addEventListener('click', function (event) {
  event.stopImmediatePropagation();
  console.log(1);
});

element.addEventListener('click', function(event) {
  // 不会被触发
  console.log(2);
});

示例

<body>
  <div id="container">
    <div id="root">
      <button id="btn">ClickMe</button>
    </div>    
  </div>
<script>  
  const phases = {
    1: 'capture',
    2: 'target',
    3: 'bubble'
  }
  container.addEventListener('click', function(e) {
    console.log('container->',phases[e.eventPhase]);
  },true);
  container.addEventListener('click', function(e) {
    console.log('container->',phases[e.eventPhase]);
  },false);
  root.addEventListener('click', function(e) {
    event.stopImmediatePropagation(); // 立即停止传播
    console.log('root->',phases[e.eventPhase]);
  },true);
  root.addEventListener('click', function(e) {
    console.log('root->',phases[e.eventPhase]);
  },false);
  btn.addEventListener('click', function(e) {
    console.log('btn->',phases[e.eventPhase]);
  }, true)
  btn.addEventListener('click', function(e) {
    console.log('btn->',phases[e.eventPhase]);
  }, false)
</script> 
View Code

运行结果如下:

// 单击btn
container->capture
root->capture

//单击root
container->capture
root->target //目标节点只能触发一次事件

3. event.preventDefault()

取消浏览器对当前事件的默认行为。该方法有效前提是事件的cancelable属性为true。

所有浏览器子有的一些事件(click,mouseover等)该属性都是true;

自定义的事件默认cancelable属性为false,需要手动设置为true。

const event = new Event('myevent', {cancelable: true})

事件的默认行为常见的有:

1)向input键入内容,会在文本框中显示输入的数据;

    通过keypress事件监听可以阻止文本输入,使无法输入。

2)单击单选框/复选框,会出现选中效果;

    通过click事件监听可以取消选中行为,使无法选中。

3)单击<a>标签的链接,会出现跳转;

    通过click事件监听可以取消跳转行为,使无法跳转。

4)空格键使页码向下滚动;

示例:

<!--
 * @Author: LyraLee
 * @Date: 2019-10-30 08:53:28
 * @LastEditTime: 2019-11-11 18:48:19
 * @Description: preventDefault()
 -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>串行异步操作</title>
  <style>
    body{
      height: 2000px;
    }
  </style>
</head>
<body>
  <input id="checkboxElement" type="checkbox"/>
  <a id="toBaidu" href="http://www.baidu.com">跳转到百度</a>
  <input id="inputElement" type="text"/>
<script>  
  checkboxElement.addEventListener('click', function(e) {
    e.preventDefault(); //无法选中
  })  
  toBaidu.addEventListener('click', function(e) {
    e.preventDefault(); // 无法跳转
  })
  inputElement.addEventListener('keypress', function(e) {
    console.log(e);
    if (e.charCode < 97 || e.charCode > 122) { 
      e.preventDefault();//键入的字符只能是a-z
      alert('only lowercase letters');
    }
  })
  document.body.addEventListener('keypress', function(e) {
    if (e.charCode === 32) {// 是空格键
      e.preventDefault(); //阻止空格后页码向下滑动
    }
  })
</script> 
</body>
</html>
View Code

4. event.composedPath()

返回一个数组,成员是目标节点到window节点的冒泡的所有路径。

示例:

<body>
  <div id="container">
    <div id="second">
      <div id='bottom'>
        ClickMe
      </div>
    </div>
  </div>
<script>  
  bottom.addEventListener('click', function(e) {
    console.log(e.composedPath()); 
    //[div#bottom, div#second, div#container, body, html, document, Window]
  },false)
</script> 

三. 事件传播相关属性

1. 只读属性

1. event.eventBubbles

返回值:布尔值;表示是否可以冒泡

浏览器原生事件默认都是true,可以冒泡;

通过Event构造函数,自定义的事件默认为false,不能冒泡,需要手动设置。

const event = new Event('lyra', {bubbles: true})

2. event.eventPhase

返回值:0-3的数字;表示当前当前事件传播所处的阶段。

0: 事件未发生
1: 事件捕获阶段
2: 目标阶段
3: 事件冒泡阶段

3. event.cancelable

返回值:布尔值;表示是否可以取消默认行为。

浏览器原生事件默认都是true,可以取消,可以调用event.preventDefault()方法;

通过Event构造函数,自定义的事件默认false, 需要手动设置后才能调用event.preventDefault();

const event = new Event('lyra', { cancelable: true})

应用:

可以用来作为使用preventDefault()方法的前置条件

  <input id="enter" type="text" />
  <script>
    enter.addEventListener('keypress', function(e) {
      if(e.cancelable) {
        e.preventDefault();
      }else {
console.warn('can not be cancelled')
} })
</script>

4. event.defaultPrevented

返回值: 布尔值;表示是否已经调用过preventDefault()方法

5.event.currentTarget

返回值:监听事件绑定的元素;相当于this; 不会变化

6.event.target

返回值:事件当前作用的元素;随触发节点实时变化;

7.event.type

返回值:事件类型

8.event.timeStamp

返回值: 毫秒时间,从网页加载完成到事件触发的时间;表示时间戳

应用:

可以通过两次mousemove触发的时间戳的差值,来计算鼠标移动速度。

var previousX;
var previousY;
var previousT;

window.addEventListener('mousemove', function(event) {
  if (
    previousX !== undefined &&
    previousY !== undefined &&
    previousT !== undefined
  ) {
    var deltaX = event.screenX - previousX;
    var deltaY = event.screenY - previousY;
    var deltaD = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));

    var deltaT = event.timeStamp - previousT;
    console.log(deltaD / deltaT * 1000);
  }

  previousX = event.screenX;
  previousY = event.screenY;
  previousT = event.timeStamp;
});
View Code

9.event.isTrusted

返回值:布尔值;表示是否是用户行为触发的,而不是用dispatch方法触发的。

一般原生事件返回true;自定义事件返回false。

10. event.detail

返回值: 事件相关信息;自定义事件中,返回用户自定义的数据

如:click事件的点击次数;滚轮的滚动距离

container.addEventListener('click', function(e) {
  console.log('container->',e.detail);
},true);
// 如果单击,返回1
// 如果双击,返回2
// 如果N次连续点击,返回N

2. 可写属性

1. event.cancelBubble

如果设为true, 可以阻止事件传播;不止阻止冒泡,也会阻止捕获;

event.cancelBubble = true; 
// 相当于
event.stopPropagation();

四.应用

1.多个字节点需要添加监听事件时

  <!--实现鼠标悬浮后,背景色修改为li便签中的颜色-->
  <ul id="container">
    <li>red</li>
    <li>yellow</li>
    <li>purple</li>
    <li>pink</li>
  </ul>
  <script>  
    // 该功能应该将监听事件添加到父节点上,否则需要添加四个监听事件
    container.addEventListener('mouseover', function(e) {
      if (e.target.tagName.toLowerCase() === 'li') {
        e.target.style.backgroundColor = e.target.innerHTML;
      } 
    })
    container.addEventListener('mouseout', function(e) { // 不能用mouseleave
      if (e.target.tagName.toLowerCase() === 'li') {
        console.log('innner');
        e.target.style.backgroundColor = '#fff';
      } 
    })
  </script> 

2. 父子节点同时添加监听事件

实际工作中,对于有复杂操作的地方,经常出现父子字节同时添加同一监听事件,但是实现不同功能的情况。

示例:

要求单击某菜单选中该菜单,将其id赋值给一个变量;菜单右侧有个提示图标,单击弹出提示框。

要求单击提示图标的时候,不选中该菜单。

分析:

要求子节点的事件不触发父节点的监听函数;使用stopPropagation();

  <ul id="container">
    <li id="menu1"><span>菜单1...</span><span class="arrow">▶️</span></li>
  </ul>
  <script>  
    let checked;
    menu1.addEventListener('click', function(e) {
      console.log(this.id);
      checked = this.id;
    })
    const arrow = document.querySelector('.arrow');
    arrow.onclick = function(e) { // 相当于addEventListen('click',fn,false)
      e.stopPropagation(); //阻止冒泡,触发父节点监听事件
      console.log('--arror---')
    }
  </script> 
原文地址:https://www.cnblogs.com/lyraLee/p/11839619.html