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
<div id="s1" style="height: 400px;width: 400px;border: 1px solid red">红
    <div id="s2" style="height: 200px;width: 200px;border: 1px solid yellow"> 黄
        <div id="s3" style="height: 100px;width: 100px;border: 1px solid blue">蓝</div>
    </div>
</div>

测试事件冒泡-点击蓝色

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)的一个方法,作用是取消一个目标元素的默认行为。既然是说默认行为,当然是元素必须有默认行为才能被取消,如果元素本身就没有默认行为,调用当然就无效了。什么元素有默认行为呢?如链接<a>,提交按钮<input type=”submit”>等。当Event 对象的 cancelable为false时,表示没有默认行为,这时即使有默认行为,调用preventDefault也是不会起作用的。

return false (jQuery)

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

jQuery源码:

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

示例:

event_propagation.png

代码如下:

<div class="box1">
    <a href="http://www.baidu.com" target="_blank"></a>
</div>
.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中:

<div id="container">
   <a href="/content1.html">content1</a>
   <a href="/content2.html">content2</a>  
   <div id="content">我会根据点击链接的url不同而改变的</div>
</div>
// 为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的事件代理给捕获到,实现了事件委托机制。

<ul id="ul">
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
  <li>6</li>
  <li>7</li>
</ul>
ul = document.getElementById('ur')

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

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

应用场景举例

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容