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 弹出窗体点击按钮返回选择数据的实现
Apr 01 Javascript
js与jQuery 获取父窗、子窗的iframe
Dec 20 Javascript
JS判断、校验MAC地址的2个实例
May 05 Javascript
jQuery $命名冲突解决方案汇总
Nov 13 Javascript
如何用js 实现依赖注入的思想,后端框架思想搬到前端来
Aug 03 Javascript
javascript实现数组去重的多种方法
Mar 14 Javascript
修改Jquery Dialog 位置的实现方法
Aug 26 Javascript
浅谈Javascript事件对象
Feb 05 Javascript
react native 文字轮播的实现示例
Jul 27 Javascript
微信小程序module.exports模块化操作实例浅析
Dec 20 Javascript
Javascript删除数组里的某个元素
Feb 28 Javascript
在vue中使用vuex,修改state的值示例
Nov 08 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在获取指定目录下的目录,在获取的目录下面再创建文件,多平台
2011/08/03 PHP
如何离线执行php任务
2017/02/21 PHP
php实现获取近几日、月时间示例
2019/07/06 PHP
jQuery学习笔记之jQuery的动画
2010/12/22 Javascript
单击和双击事件的冲突处理示例代码
2014/04/03 Javascript
javascript if条件判断方法小结
2014/05/17 Javascript
禁用Enter键表单自动提交实现代码
2014/05/22 Javascript
js实现正则匹配中文标点符号的方法
2015/12/23 Javascript
AngularJS初始化静态模板详解
2016/01/14 Javascript
基于jQuery实现仿51job城市选择功能实例代码
2016/03/02 Javascript
使用bootstrap typeahead插件实现输入框自动补全之问题及解决办法
2016/07/07 Javascript
浅谈jquery.form.js的ajaxSubmit和ajaxForm的使用
2016/09/09 Javascript
iscroll-probe实现下拉刷新和下拉加载效果
2017/06/28 Javascript
Vue-cli 使用json server在本地模拟请求数据的示例代码
2017/11/02 Javascript
JS实现textarea通过换行或者回车把多行数字分割成数组并且去掉数组中空的值
2018/10/29 Javascript
通过jQuery学习js类型判断的技巧
2019/05/27 jQuery
javascript实现5秒倒计时并跳转功能
2019/06/20 Javascript
微信小程序实现多张图片上传功能
2020/11/18 Javascript
[48:02]Ti4循环赛第三日 VG vs Liquid和NEWBEE vs DK
2014/07/12 DOTA
Python实现excel转sqlite的方法
2017/07/17 Python
用python结合jieba和wordcloud实现词云效果
2017/09/05 Python
Python用for循环实现九九乘法表
2018/05/31 Python
pandas 实现字典转换成DataFrame的方法
2018/07/04 Python
idea创建springMVC框架和配置小文件的教程图解
2018/09/18 Python
python如何爬取网站数据并进行数据可视化
2019/07/08 Python
Python 字符串、列表、元组的截取与切片操作示例
2019/09/17 Python
Python3实现mysql连接和数据框的形成(实例代码)
2020/01/17 Python
python nohup 实现远程运行不宕机操作
2020/04/16 Python
TensorFlow-gpu和opencv安装详细教程
2020/06/30 Python
无需JS和jQuery代码实现CSS3鼠标浮动放大图片
2016/11/21 HTML / CSS
荷兰游戏商店:Allyouplay
2019/03/16 全球购物
将n个数按输入顺序的逆序排列,用函数实现
2012/11/14 面试题
Python3 多线程(连接池)操作MySQL插入数据
2021/06/09 Python
JavaScript获取URL参数的方法分享
2022/04/07 Javascript
MySQL事务操作的四大特性以及并发事务问题
2022/04/12 MySQL
Python Matplotlib绘制等高线图与渐变色扇形图
2022/04/14 Python