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分页代码实例分享(js分页)
Dec 13 Javascript
jquery访问ashx文件示例代码
Aug 11 Javascript
JavaScript使用位运算符判断奇数和偶数的方法
Jun 01 Javascript
Boostrap模态窗口的学习小结
Mar 28 Javascript
Javascript的比较汇总
Jul 25 Javascript
js实现html table 行,列锁定的简单实例
Oct 13 Javascript
微信小程序实现多个按钮toggle功能的实例
Jun 13 Javascript
Puppeteer环境搭建的详细步骤
Sep 21 Javascript
vue中使用protobuf的过程记录
Oct 26 Javascript
Bootstrap实现省市区三级联动(亲测可用)
Jul 26 Javascript
Vue获取页面元素的相对位置的方法示例
Feb 05 Javascript
vue同个按钮控制展开和折叠同个事件操作
Jul 29 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
DW中链接mysql数据库时,建立字符集中文出现乱码的解决方法
2010/03/27 PHP
PHP操作XML作为数据库的类
2010/12/19 PHP
php三维数组去重(示例代码)
2013/11/26 PHP
php正则表达式验证(邮件地址、Url地址、电话号码、邮政编码)
2016/03/14 PHP
Redis使用Eval多个键值自增的操作实例
2016/11/04 PHP
php和asp语法上的区别总结
2019/05/12 PHP
jquery 表单下所有元素的隐藏
2009/07/25 Javascript
Jquery作者John Resig自己封装的javascript 常用函数
2009/11/09 Javascript
JS与框架页的操作代码
2010/01/17 Javascript
jquery中trigger()无法触发hover事件的解决方法
2015/05/07 Javascript
javascript实现tab切换特效
2015/11/12 Javascript
ajax异步请求详解
2017/01/06 Javascript
微信小程序 下拉菜单的实现
2017/04/06 Javascript
Agularjs妙用双向数据绑定实现手风琴效果
2017/05/26 Javascript
js中url对象化管理分析
2017/12/29 Javascript
基于iview的router常用控制方式
2019/05/30 Javascript
一看就会的vuex实现登录验证(附案例)
2020/01/09 Javascript
Vue的自定义组件不能使用click方法的解决
2020/07/28 Javascript
python实现爬取千万淘宝商品的方法
2015/06/30 Python
Python处理文本文件中控制字符的方法
2017/02/07 Python
用Python实现最速下降法求极值的方法
2019/07/10 Python
nginx+uwsgi+django环境搭建的方法步骤
2019/11/25 Python
pyinstaller将含有多个py文件的python程序做成exe
2020/04/29 Python
基于CSS3实现图片模糊过滤效果
2015/11/19 HTML / CSS
css3遮罩层镂空效果的多种实现方法
2020/05/11 HTML / CSS
Booking.com德国:预订最好的酒店和住宿
2020/02/16 全球购物
电子商务专业自我鉴定
2013/12/18 职场文书
十佳班主任事迹材料
2014/01/18 职场文书
2014年大学生就业规划书
2014/04/04 职场文书
小学生手册家长评语
2014/04/16 职场文书
2014旅游局领导班子四风问题对照检查材料思想汇报
2014/09/19 职场文书
离婚协议书范文
2015/01/26 职场文书
公司聚餐通知
2015/04/22 职场文书
JS继承最简单的理解方式
2021/03/31 Javascript
MySQL Threads_running飙升与慢查询的相关问题解决
2021/05/08 MySQL
vue3获取当前路由地址
2022/02/18 Vue.js