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 相关文章推荐
js中prototype用法详细介绍
Nov 14 Javascript
js获取会话框prompt的返回值的方法
Jan 10 Javascript
解决jQuery上传插件Uploadify出现Http Error 302错误的方法
Dec 18 Javascript
jQuery实现的超链接提示效果示例【附demo源码下载】
Sep 09 Javascript
AngularJS中run方法的巧妙运用
Jan 04 Javascript
深入理解ES6之数据解构的用法
Jan 13 Javascript
浅谈手写node可读流之流动模式
Jun 01 Javascript
JavaScript高阶教程之“==”隐藏下的类型转换
Apr 11 Javascript
layer.confirm()右边按钮实现href的例子
Sep 27 Javascript
jQuery轮播图功能制作方法详解
Dec 03 jQuery
js实现文字头像的生成代码
Mar 07 Javascript
利用js canvas实现五子棋游戏
Oct 11 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
php 用sock技术发送邮件的函数
2007/07/21 PHP
PHP实例分享判断客户端是否使用代理服务器及其匿名级别
2014/06/04 PHP
PHP获取表单数据与HTML嵌入PHP脚本的实现
2017/02/09 PHP
js的正则test,match,exec详细解析
2014/01/29 Javascript
jquery操作select详解(取值,设置选中)
2014/02/07 Javascript
跟我学Nodejs(一)--- Node.js简介及安装开发环境
2014/05/20 NodeJs
javascript 小数取整简单实现方式
2014/05/30 Javascript
JavaScript和JQuery的鼠标mouse事件冒泡处理
2015/06/19 Javascript
JS简单实现城市二级联动选择插件的方法
2015/08/19 Javascript
jquery实现鼠标经过显示下划线的渐变下拉菜单效果代码
2015/08/24 Javascript
第十章之巨幕页头缩略图与警告框组件
2016/04/25 Javascript
使用Node.js给图片加水印的方法
2016/11/15 Javascript
详解vue-cli项目中用json-sever搭建mock服务器
2017/11/02 Javascript
解决Vue 项目打包后favicon无法正常显示的问题
2018/09/01 Javascript
详解Js里的for…in和for…of的用法
2019/03/28 Javascript
浅谈VUE防抖与节流的最佳解决方案(函数式组件)
2019/05/22 Javascript
js如何实现元素曝光上报
2019/08/07 Javascript
layui当点击文本框时弹出选择框,显示选择内容的例子
2019/09/02 Javascript
js实现简单的点名器随机色实例代码
2020/09/20 Javascript
[02:58]魔廷新尊——痛苦女王至宝语音台词节选
2020/06/14 DOTA
python共享引用(多个变量引用)示例代码
2013/12/04 Python
Linux 发邮件磁盘空间监控(python)
2016/04/23 Python
JSON Web Tokens的实现原理
2017/04/02 Python
Python实现 PS 图像调整中的亮度调整
2019/06/28 Python
django 自定义filter 判断if var in list的例子
2019/08/20 Python
Cython编译python为so 代码加密示例
2019/12/23 Python
Python 识别12306图片验证码物品的实现示例
2020/01/20 Python
python ImageDraw类实现几何图形的绘制与文字的绘制
2020/02/26 Python
spyder 在控制台(console)执行python文件,debug python程序方式
2020/04/20 Python
详解CSS中iconfont的使用
2015/08/04 HTML / CSS
HTML5 Web Database 数据库的SQL语句的使用方法
2012/12/09 HTML / CSS
英国文胸专家:AmpleBosom.com
2018/02/06 全球购物
如何写毕业求职自荐信
2013/11/06 职场文书
电气工程及自动化专业自荐书范文
2013/12/18 职场文书
毕业自我鉴定怎么写
2014/03/25 职场文书
vue 实现上传组件
2021/05/31 Vue.js