js基础之事件捕获与冒泡原理


Posted in Javascript onOctober 09, 2019

想要了解什么是事件捕获与冒泡,需要先了解什么是事件。

什么是事件?

我们知道,在前端开发中,JavaScript负责定义网页的“行为”。这里所说的“定义”,其实指的是开发者可以通过JavaScript语言向浏览器描述一些规则,浏览器按照这些规则与用户进行交互。比如开发者希望当用户点击页面上某个按钮的时候,就弹出一个窗口,显示特定的内容。而当用户真正点击这个按钮的时候,浏览器将按照开发者定义的这个规则,去弹出指定的窗口,显示指定的内容。

在上面的例子中,浏览器是一切规则的执行者,开发者是这些规则的制定者,而JavaScript只是开发者向浏览器描述这些规则时所使用的的语言(否则浏览器无法知道开发者想要在什么情况下做什么事)。假如我们通过以下的语句向浏览器描述了一条规则:

<body>
 <button id="btn">点击</button>
 <script>
 var button = document.getElementById("btn"); //获取页面上的按钮
 button.addEventListener("click", function(){ //定义点击事件
 alert("我被点击了");
 })
 </script>
</body>

js基础之事件捕获与冒泡原理

页面上现在有一个按钮,我们首先使用原生DOM获取这个按钮,然后使用button.addEventListener(“click”, function(){})这样的语法向浏览器描述了一条规则:当这个按钮被点击(click)时,弹出提示框,显示“我被点击了”。用户点击按钮后网页就会出现如下提示:

js基础之事件捕获与冒泡原理

浏览器把这次“点击”称为一个“事件”。“事件”用于描述交互过程中某些特定的关键点(如点击、鼠标滑动、滚轮滚动、按下键盘、触屏操作等,每个操作都对应特定的事件,不过事件也可能与用户行为无关,比如网页加载完毕也是一个事件)。而浏览器处理交互最重要的手段就是基于事件来执行开发者定义好的回调函数(如在用户“点击按钮”时“弹出窗口”,而定义“弹出窗口”行为的就是回调函数,也就是addEventListener中的function)。

定义完这条规则,当用户点击按钮时,浏览器就会弹出上述窗口了。我们称“点击”这个事件是在这个按钮上触发的(因为我们的回调函数是绑定在这个按钮上的)。

那什么是事件的捕获与冒泡呢?

事件的捕获与冒泡

这个问题与HTML的结构息息相关。

在前端开发中,我们使用标签语言HTML来描述网页结构,如一个标题、一个段落、一个表格等,这些网页元素描述了网页上有哪些需要显示的内容,它们构成了整个网页的“骨骼”,通常是一种嵌套的结构,比如:

<html>
 <head>
 ... //这是对网页内容的元描述
 </head>
 
 <body> //这是网页需要渲染的真正内容
 <div>
  <h1>标题</h1>
  <p>这里是一个段落</p>
 </div>
 </body>
</html>

上述网页结构示意图如下(在没有设置padding等属性的情况下,子元素通常会填满父元素,这里的内间距只是为了说明元素的嵌套关系):

js基础之事件捕获与冒泡原理

我们看到,body元素是整个网页的容器,它的内部包含了一个div元素,而div的内部又包含了两个元素:h1和p。假如我们现在在p的内部点击了一下,那么请问我们有没有点击它的外部容器div,以及最外部的body呢?

从浏览器的角度来看,我们同时在点击这三个元素。

想要证明这个结论非常简单,只需要使用addEventListener向div和body各自绑定click事件,如果点击p时也会被触发,那就说明上面的结论是正确的。毫无疑问,它们会被触发。

那么问题来了,既然用户同时在点击这三个元素,浏览器应该先执行哪个元素定义的回调函数呢(由于JavaScript采用单线程模型,执行回调函数必然有一定的先后顺序)?

这个问题实际上是在说,对于嵌套的元素,应该从内向外还是从外向内响应事件。浏览器之争的两大对立方分别有自己的看法:Netscape公司认为应当由最外层的body首先得到这个事件,其次是div,最后才是目标元素p;而微软的IE开发组则认为,应当是内部的p首先得到这个事件,然后是div,最后才是body。在没有标准约束的情况下,两者按照自己的想法去设计浏览器的事件模型,Netscape从外向内传播的模型在业内被称为事件捕获模型,而微软从内向外传播的模型则被称为事件冒泡模型。

两个模型虽然从思路上南辕北辙,但是都可以保证所有绑定的回调函数正确触发(不过触发顺序是相反的。如果这个触发顺序很重要,那么在当时,你的代码可能只能在一个浏览器中正确运行,或者去做恶心的浏览器兼容)。不过浏览器允许开发者在事件传播的过程中阻止事件的继续传播,此时两者的差异就变得极其明显。

假如我们在定义点击div元素的回调函数时阻止了事件的传播:

div.addEventListener("click", function(e){
 ...
 e.stopPropagation(); //阻止事件继续传播
})

这个代码会在两种模型下产生巨大的差异。在捕获模型中,由于最外部首先得到该事件,因此body的点击事件首先被触发,之后是div的点击事件。由于阻止了事件传播,p元素不会触发回调。而在冒泡模型中则恰恰相反,内部的p首先得到该事件,其次才是div,因此触发回调的将是p和div,body因为事件没有冒泡上来而无法监听到该事件。同样的代码在两种模型中产生了完全不同的行为,这对于开发者来说显然是不可接受的(两个模型都有自己的适用场景,也都有自己的合理性,因此对于模型的好坏不能一概而论)。

那么后来的国际标准组织是如何解决这个冲突的呢?答案就是由开发者自己选择。

标准的事件绑定使用addEventListener函数,它接收两个必传参数和一个可选参数:必传的为event(事件名,如"cick")和function(回调函数),可选的为useCapture(是否使用捕获模型,默认为false,根据MDN的接口说明,这里也可以传入一个对象,为本次监听设置其他参数,详细请参考MDN接口文档 - addEventListener)。

div.addEventListener("click", function(){}, true); //使用捕获模型

第三个参数就是标识开发者是否需要使用捕获模型,默认为false,也就是默认使用微软的冒泡模型(这是因为大多数事件都只在最内部的元素上触发,这也间接表明,冒泡模型的普适性更好)。如果开发者的需求确实需要使用捕获模型,可以将第三个参数设置为true。比如下面的例子:

事件捕获与冒泡的用法

了解了事件捕获与冒泡的基本原理之后,我们举个例子来说明这两个模型的基本用法。

假设有以下的DOM结构:

<div id="outer">
 <div id="inner" style="width:100px;height: 100px;border: 1px solid black;">
 
 </div>
 </div>

这是两个重叠的div,当点击时,两者都会响应这个click事件。假如事件绑定如下:

var outer = document.querySelector("#outer");
var inner = document.querySelector("#inner");
 outer.addEventListener("click", function(e){
 alert("来自外部div的消息");
 e.stopPropagation(); //阻止事件向内部传播
 }, true); //使用捕获模型

 inner.addEventListener("click", function(e){
 alert("来自内部div的消息");
 }, true); //使用捕获模型

页面上将只显示外部弹出的消息,内部的事件被e.stopPropagation()拦截了下来,导致事件没有触发。而如果写成下面的代码:

var outer = document.querySelector("#outer");
var inner = document.querySelector("#inner");
 outer.addEventListener("click", function(e){
 alert("来自外部div的消息");
 }, false); //使用冒泡模型

 inner.addEventListener("click", function(e){
 alert("来自内部div的消息");
 e.stopPropagation(); //阻止事件向外部传播
 }, false); //使用冒泡模型

这次是只显示了内部的消息,而没有显示外部的消息,说明事件在向上冒泡的过程中被阻止了。

注意

如果是在表格中内嵌复选框,希望实现点击一行时选中复选框,通过stopPropagation阻止CheckBox响应click事件并不能实现。测试发现复选框状态改变的事件似乎并不是在click事件触发的(断点跟踪表明,CheckBox在执行click回调之前,状态就已经发生了改变,具体是通过什么事件改变了选中状态尚不清楚),下面给一个可以处理行点击的示例:

<table border="1" cellspacing="0">
 <tr class="tr">
 <td>
 <input class="checkbox" type="checkbox">
 
 </td>
 <td>
 表格第一行
 </td>
 </tr>
 <tr class="tr">
 <td>
 <input class="checkbox" type="checkbox">
 </td>
 <td>
 表格第二行
 </td>
 </tr>
</table>
<script>
 var tr = document.querySelectorAll(".tr"); //获取所有tr
 tr.forEach(function(item){ //为每个tr绑定click事件,手动选中复选框
 item.addEventListener("click", function(e){
 var checkbox = item.querySelector(".checkbox");
 checkbox.checked = !checkbox.checked;
 })
 })

 var cb = document.querySelectorAll(".checkbox");
 cb.forEach(function(item){
 item.addEventListener("click", function(e){
 this.checked = !this.checked;
 });
 })
</script>

这里没有使用stopPropagation阻止事件传播,而是通过为CheckBox定义额外的click事件来解决状态不变的问题(经过断点跟踪,此时在点击CheckBox时,状态发生了三次变化,第一次是触发了某个原生事件导致其状态变化,第二次是执行了tr的点击事件,第三次则是为CheckBox自定义的click事件)。也就是说,点击tr时状态改变一次,点击CheckBox时状态改变三次,功能均正常。

由于在大多数情况下,事件都是由最内层的元素来处理的,所以冒泡模型的应用更为广泛,它也因此成为绑定事件时使用的默认模型。

总结

事件的捕获与冒泡两个模型相对比较简单,只要明白了其中的原理,就可以很容易掌握通过stopPropagation阻止事件传播的使用。

浏览器的标准事件模型把事件的传播过程分成了三个阶段:捕获阶段、处于目标阶段和冒泡阶段。捕获阶段指事件从最外层传播到最内层之前的整个过程,对应捕获模型;处于目标阶段指的是事件刚好传播到目标元素上;而冒泡阶段指的是从最内层元素向外传播的整个过程。所以我们看到,标准的浏览器事件模型就是把捕获模型和冒泡模型有机地结合起来,使开发者可以以最简单的方式灵活地使用两个模型。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Javascript中Eval函数的使用说明
Oct 11 Javascript
禁止JQuery中的load方法装载IE缓存中文件的方法
Sep 11 Javascript
js时间戳格式化成日期格式的多种方法
Nov 11 Javascript
Javascript动态引用CSS文件的2种方法介绍
Jun 06 Javascript
jquery对复选框(checkbox)的操作汇总
Jan 13 Javascript
JS for循环中i++ 和 ++i的区别介绍
Jul 20 Javascript
基于Vue如何封装分页组件
Dec 16 Javascript
浅谈jQuery操作类数组的工具方法
Dec 23 Javascript
JS中绑定事件顺序(事件冒泡与事件捕获区别)
Jan 24 Javascript
JQuery form表单提交前验证单选框是否选中、删除记录时验证经验总结(整理)
Jun 09 jQuery
详解js模板引擎art template数组渲染的方法
Oct 09 Javascript
夯基础之手撕javascript继承详解
Nov 09 Javascript
微信内置浏览器图片查看器的代码实例
Oct 08 #Javascript
vue封装swiper代码实例解析
Oct 08 #Javascript
jQuery实现提交表单时不提交隐藏div中input的方法
Oct 08 #jQuery
简单实现节流函数和防抖函数过程解析
Oct 08 #Javascript
微信小程序返回箭头跳转到指定页面实例解析
Oct 08 #Javascript
AntV F2和vue-cli构建移动端可视化视图过程详解
Oct 08 #Javascript
vue.js中ref及$refs的使用方法解析
Oct 08 #Javascript
You might like
农民和部队如何穿矿
2020/03/04 星际争霸
海河写的 Discuz论坛帖子调用js的php代码
2007/08/23 PHP
ajax在joomla中的原生态应用代码
2012/07/19 PHP
PHP程序中的文件锁、互斥锁、读写锁使用技巧解析
2016/03/21 PHP
php使用curl下载指定大小的文件实例代码
2017/09/30 PHP
搜索附近的人PHP实现代码
2018/02/11 PHP
laravel获取不到session的三种解决办法【推荐】
2018/09/16 PHP
PHP htmlspecialchars() 函数实例代码及用法大全
2018/09/18 PHP
PHP递归算法的简单实例
2019/02/28 PHP
ThinkPHP5&amp;5.1框架关联模型分页操作示例
2019/08/03 PHP
jQuery 获取URL参数的插件
2010/03/04 Javascript
利用js判断浏览器类型(是否为IE,Firefox,Opera浏览器)
2013/11/22 Javascript
Jquery在指定DIV加载HTML示例代码
2014/02/17 Javascript
jquery获取一个元素下面相同子元素的个数代码
2014/07/31 Javascript
javascript图片滑动效果实现
2021/01/28 Javascript
怎么引入(调用)一个JS文件
2016/05/26 Javascript
详解Vue2.0里过滤器容易踩到的坑
2017/06/01 Javascript
基于vue组件实现猜数字游戏
2020/05/28 Javascript
vue.js中toast用法及使用toast弹框的实例代码
2018/08/27 Javascript
详解多页应用 Webpack4 配置优化与踩坑记录
2018/10/16 Javascript
fetch 如何实现请求数据
2018/12/20 Javascript
layer扩展打开/关闭动画的方法
2019/09/23 Javascript
通过python下载FTP上的文件夹的实现代码
2013/02/10 Python
Python运行报错UnicodeDecodeError的解决方法
2016/06/07 Python
numpy中loadtxt 的用法详解
2018/08/03 Python
使用PyQtGraph绘制精美的股票行情K线图的示例代码
2019/03/14 Python
Python经纬度坐标转换为距离及角度的实现
2020/11/01 Python
查找适用于matplotlib的中文字体名称与实际文件名对应关系的方法
2021/01/05 Python
波兰在线运动商店:YesSport
2020/07/23 全球购物
自考生自我评价分享
2014/01/18 职场文书
2014年商场超市庆元旦活动方案
2014/02/14 职场文书
授权委托书
2014/07/31 职场文书
因个人工作失误检讨书
2019/06/21 职场文书
Python使用PyYAML库读写yaml文件的方法
2022/04/06 Python
Python之matplotlib绘制饼图
2022/04/13 Python
MySQL 原理优化之Group By的优化技巧
2022/08/14 MySQL