js的事件传播机制

1. 什么是事件传播?

  • 事件传播(event propagation)是事件冒泡(event bubbling)和事件捕获(event capturing)的统称:

  • 事件冒泡:事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点。在事件冒泡阶段只有标记为非捕获的监听器才会被调用。也就是那些调用 addEventListener() 时第三个参数为 false 时注册的监听器。默认值即为false。

  • 事件捕获:不太具体的DOM节点应该更早接收到事件,而最具体的节点应该最后接收到事件。在这一阶段,只有那些设置为捕获阶段工作的监听器才被调用。要为捕获阶段设置监听器,可以在调用 addEventListener 时设置第三个参数为 true:

  • demo

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>事件传播 demo</title>
</head>
<body>
    <div>
        <button>
            <span>点击span标签</span>
        </button>
    </div>
<script>
    const oP = document.querySelector('span');
    const oB = document.querySelector('button');
    const oD = document.querySelector('div');
    const oBody = document.querySelector('body');

    // -----捕获
    oP.addEventListener('click',function(){
        console.log('span标签被点击---捕获')
    },true);

    oB.addEventListener('click',function(){
        console.log("button被点击---捕获")
    },true);

    oD.addEventListener('click',  function(){
        console.log('div被点击---捕获')
    },true);

    oBody.addEventListener('click',function(){
        console.log('body被点击---捕获')
    },true);

    window.addEventListener('click',function(){
        console.log('window被点击---捕获')
    },true);

    // -----冒泡
    oP.addEventListener('click',function(){
        console.log('span标签被点击---冒泡')
    },false);

    oB.addEventListener('click',function(){
        console.log("button被点击---冒泡")
    },false);

    oD.addEventListener('click',  function(){
        console.log('div被点击---冒泡')
    },false);

    oBody.addEventListener('click',function(){
        console.log('body被点击---冒泡')
    },false);

    window.addEventListener('click',function(){
        console.log('window被点击---冒泡')
    },false);
</script>
</body>
</html>

image.png

  • 事件传播是双向的,从window到事件目标,然后再返回。这种传播可以分为三个阶段:

CAPTURING_PHASE 捕获阶段 — 事件捕获:从window到事件目标的父级元素
AT_TARGET 目标阶段(当前事件源) — 事件目标,在此阶段,将调用在事件目标上注册的所有监听器,而不管其捕获标志的值如何。
BUBBLING _PHASE :冒泡阶段 — 事件冒泡:从事件目标的父级元素回到 window

image.png

2.访问事件传播信息

每次触发DOM事件时会产生一个事件对象(也称event对象),此处的参数e接收事件对象。

  • event.target 指向事件目标
  • event.currentTarget 是正在执行的监听器注册到的节点
  • event.eventPhase 得知当前的阶段。它的值是一个数字,1到3分别对应的是 Event 构造函数的常量 CAPTURING_PHASE, AT_TARGET 和 BUBBLING_PHASE

3.停止事件传播

  • 事件传播是可以终止的,只需要在任意监听器中调用 event 对象的 stopPropagation() 方法。这意味着传播路径中当前节点之后节点上的所有监听器不会被调用了。不过,绑定在当前目标上的其他剩余监听器仍将调用。
event.stopPropagation()
  • 立即停止传播:除了 stopPropagation,还有 stopImmediatePropagation。正如它的名字所表明的那样,会立即停止事件传播,甚至阻止当前节点上的其他监听器被调用。
    如果说在事件捕获阶段,将子节点移除,那么子节点的捕获和冒泡是否还会执行?
    事实上,执行顺序没有变化,子节点的捕获和冒泡依然执行,这里就需要我们做一些优化了,不仅要移除子节点,还需要对节点的注册事件进行移除。
event.stopImmediatePropagation()
  • 阻止默认事件传播:有些事件会在传播结束后执行一些默认的浏览器操作。例如,单击一个链接或单击表单提交按钮分别会使浏览器导航到新页面和提交表单。通过在监听器中调用event对象的另一个方法 preventDefault(),可以避免浏览器执行默认的操作。
event.preventDefault()

4.事件监听执行顺序

问题:当前dom节点存在多个事件监听时,如何准确阻止事件传播?
总的来讲,同一dom事件监听可以分为两种情况:
1)为当前dom节点绑定同样的事件监听,如为div创建2个click事件监听:

DOM0级事件具有极好的跨浏览器优势,会以最快的速度绑定

<div id="box">点我</div>
<script>
	// 某一个元素的同一个行为绑定不同的方法在script标签中后面的方法会覆盖前面的方法
	var box = document.getElementById('box');
	box.onclick = fun1;
	box.onclick = fun2;
	function fun1() {
		console.log('方法1');
	}
	function fun2() {
		console.log('方法2');
	}
	// 执行方法2;方法2覆盖方法1,所以方法1不执行
</script>

DOM2级事件是通过addEventListener绑定的事件,IE下的DOM2事件通过attachEvent绑定;可以给某一个元素的同一个行为绑定不同的方法在行内会分别执行

<div id="box">点我</div>
<script>
 	var box = document.getElementById('box');
	box.addEventListener('click', fun1,false);
	box.addEventListener('click', fun2,false);
	function fun1() {
		console.log('方法1');
	}
	function fun2() {
		console.log('方法2');
	}
	// 执行方法1		// 执行方法2
</script>

2)为当前dom节点绑定不同的事件监听
mousedown: 当鼠标指针移动到元素上方,并按下鼠标按键(左、右键均可)时,会发生 mousedown 事件。
与 click 事件不同,mousedown 事件仅需要按键被按下,而不需要松开即可发生。

mouseup:当在元素上松开鼠标按键(左、右键均可)时,会发生 mouseup 事件。
与 click 事件不同,mouseup 事件仅需要松开按钮。当鼠标指针位于元素上方时,放松鼠标按钮就会触发该事件。

click:当鼠标指针停留在元素上方,然后按下并松开鼠标左键时,就会发生一次 click 事件。
注意:触发click事件的条件是按下并松开鼠标左键!,按下并松开鼠标右键并不会触发click事件。

blur:当元素失去焦点时触发blur事件;其为表单事件,blur和focus事件不会冒泡,其他表单事件都可以。

<div style="margin-top: 20px;">
    <input type="text">
    <button id="btn">button</button>
</div>
<script>
// mousedown->blur->mouseup->click
var btn = document.getElementById('btn');
var input = document.getElementsByTagName('input')[0];
btn.addEventListener('mousedown', function() {
    console.log('>>>>>>>>>>>>>>>>>>> mousedown');
})
btn.addEventListener('mouseup', function() {
    console.log('>>>>>>>>>>>>>>>>>>> mouseup');
})
btn.addEventListener('click', function() {
    console.log('>>>>>>>>>>>>>>>>>>> click');
})
input.addEventListener('blur', function() {
    console.log('>>>>>>>>>>>>>>>>>>> blur');
})
</script>

若在同一个元素上按下并松开鼠标左键,会依次触发mousedown、mouseup、click,前一个事件执行完毕才会执行下一个事件
若在同一个元素上按下并松开鼠标右键,会依次触发mousedown、mouseup,前一个事件执行完毕才会执行下一个事件,不会触发click事件

所以总结事件顺序应为:mousedown->(other)blur->mouseup->click

补充: js、jquery 中 blur事件和click事件冲突
问题说明:
在表单验证中,我们往往会在输入框失去焦点时触发一个blur事件,但当失去焦点后点击了一个按钮,这时blur事件和click事件就都触发了,由于js是单线程的所以就出现了问题,现在需要让blur先执行验证,然后在触发click事件。

解决方法:
给按钮的click事件设置延迟执行setTimeOut(fn,100),延迟时间的设置要大于blur事件的执行时间,这样就会在blur事件执行完后在执行click事件。或者用mousedown监听替代click。

5.事件代理(事件委托)

问题:不知道大家在平时的使用的时候有没有遇到过这样的一种情况,如果事件涉及到更新HTML节点或者添加HTML节点的时候,就会出现这样的一种情况,新添加的节点无法绑定事件,更新的节点也是无法绑定事件,表现的行为是无法触发事件

方案:由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。

  • 优点
  1. 减少内存消耗,提高性能;假设有一个列表,列表之中有大量的列表项,我们需要在点击每个列表项的时候响应一个事件。如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。借助事件代理,我们只需要给父容器ul绑定方法即可,这样不管点击的是哪一个后代元素,都会根据冒泡传播的传递机制,把容器的click行为触发,然后把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而完成不同的事。
  2. 动态绑定事件;在很多时候,我们需要通过用户操作动态的增删列表项元素,如果一开始给每个子元素绑定事件,那么在列表发生变化时,就需要重新给新增的元素绑定事件,给即将删去的元素解绑事件,如果用事件代理就会省去很多这样麻烦。
  • 如何实现
// 将父层元素 #list 下的 li 元素的事件委托到它的父层元素上:
// 给父层元素绑定事件
document.getElementById('list').addEventListener('click', function (e) {
  // 兼容性处理
  var event = e || window.event;
  var target = event.target || event.srcElement;
  // 判断是否匹配目标元素
  if (target.nodeName.toLocaleLowerCase === 'li') {
    console.log('the content is: ', target.innerHTML);
  }
});

6.拓展

  • 事件监听常用的属性含义:
属性描述DOM
bubbles返回布尔值,指示事件是否是起泡事件类型。2
cancelable返回布尔值,指示事件是否可拥可取消的默认动作。2
currentTarget返回其事件监听器触发该事件的元素。在事件程序中,this和currentTarget指代的是同一对象。2
eventPhase返回事件传播的当前阶段。调用事件处理程序的阶段:1 捕获;2 处于阶段;3 冒泡阶段;这个属性的变化需要在断点中查看,不然你看到的总是02
target返回触发此事件的元素(事件的目标节点)。2
timeStamp返回事件生成的日期和时间。2
type返回当前 Event 对象表示的事件的名称。2
view与事件关联的抽象视图,发生事件的window对象2
preventDefault取消事件默认行为,cancelable是true时可以使用2
stopPropagation取消事件捕获/冒泡,bubbles为true才能使用2
stopImmediatePropagation取消事件进一步冒泡,并且组织任何事件处理程序被调用3
  • 兼容性
事件冒泡事件捕获
IE和标准浏览器(ie8以下只支持事件冒泡,不支持事件捕获)标准浏览器
.addEventListener(‘click’,function(){},false).attachEvent(‘onclick’,function(){})
三个参数:
1.事件类型,没有on
2.事件处理函数
3.布尔型的数值,默认false(事件冒泡),true(事件捕获)
有两个参数:
1.事件类型,有on
2.事件处理函数
阻止事件冒泡/捕获 : e.stopPropagation();阻止事件冒泡 : window.event,cancelBubble=true
  • EventTarget.addEventListener()

target.addEventListener(type, listener, options);
target.addEventListener(type, listener, useCapture);

type
表示监听事件类型的字符串。

listener
当所监听的事件类型触发时,会接收到一个事件通知(实现了 Event 接口的对象)对象。listener 必须是一个实现了 EventListener 接口的对象,或者是一个函数。

options 可选
一个指定有关 listener 属性的可选参数对象。可用的选项如下:
capture: Boolean,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。
once: Boolean,表示 listener 在添加之后最多只调用一次。如果是 true, listener 会在其被调用之后自动移除。
passive: Boolean,设置为true时,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。

useCapture 可选
Boolean,在DOM树中,注册了listener的元素, 是否要先于它下面的EventTarget,调用该listener。 当useCapture(设为true) 时,沿着DOM树向上冒泡的事件,不会触发listener。当一个元素嵌套了另一个元素,并且两个元素都对同一事件注册了一个处理函数时,所发生的事件冒泡和事件捕获是两种不同的事件传播方式。事件传播模式决定了元素以哪个顺序接收事件。如果没有指定, useCapture 默认为 false 。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

萝卜砸大坑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值