事件的冒泡和捕获
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>
运行结果如下:
// 单击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>
运行结果如下:
// 单击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>
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; });
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>