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代码
Nov 09 Javascript
微信小程序 navigation API实例详解
Oct 02 Javascript
JavaScript基于对象去除数组重复项的方法
Oct 09 Javascript
Node.js 异步异常的处理与domain模块解析
May 10 Javascript
AngularJS中scope的绑定策略实例分析
Oct 30 Javascript
Node.js使用MySQL连接池的方法实例
Feb 11 Javascript
vuex与组件联合使用的方法
May 10 Javascript
vue-cli3.0使用及部分配置详解
Aug 29 Javascript
JS校验与最终登陆界面功能完整示例
Jan 13 Javascript
详解vue 组件
Jun 11 Javascript
Vue组件生命周期运行原理解析
Nov 25 Vue.js
vue 使用rules对表单字段进行校验的步骤
Dec 25 Vue.js
微信内置浏览器图片查看器的代码实例
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 print类函数使用总结
2010/06/25 PHP
php中使用gd库实现远程图片下载实例
2015/05/12 PHP
基于Laravel-admin 后台的自定义页面用法详解
2019/09/30 PHP
模仿JQuery sortable效果 代码有错但值得看看
2009/11/05 Javascript
基于jQuery的获得各种控件Value的方法
2010/11/19 Javascript
JavaScript window.document的属性、方法和事件小结
2012/10/24 Javascript
原生js获取宽高与jquery获取宽高的方法关系对比
2014/04/04 Javascript
Javascript中的getUTCHours()方法使用详解
2015/06/10 Javascript
node.js中格式化数字增加千位符的几种方法
2015/07/03 Javascript
js 声明数组和向数组中添加对象变量的简单实例
2016/07/28 Javascript
Angular.Js的自动化测试详解
2016/12/09 Javascript
前端html中jQuery实现对文本的搜索功能并把搜索相关内容显示出来
2017/11/14 jQuery
js利用iframe实现选项卡效果
2020/08/09 Javascript
Python运行的17个时新手常见错误小结
2012/08/07 Python
Python单元测试框架unittest使用方法讲解
2015/04/13 Python
Python闭包实现计数器的方法
2015/05/05 Python
简单易懂的python环境安装教程
2017/07/13 Python
谈谈python中GUI的选择
2018/03/01 Python
Python 获取ftp服务器文件时间的方法
2019/07/02 Python
Python画图实现同一结点多个柱状图的示例
2019/07/07 Python
django Admin文档生成器使用详解
2019/07/22 Python
Python 调用 Windows API COM 新法
2019/08/22 Python
python数据预处理 :数据抽样解析
2020/02/24 Python
利用Python实现字幕挂载(把字幕文件与视频合并)思路详解
2020/10/21 Python
html5 Canvas画图教程(2)—画直线与设置线条的样式如颜色/端点/交汇点
2013/01/09 HTML / CSS
HTML5 video 事件应用示例
2014/09/11 HTML / CSS
canvas实现高阶贝塞尔曲线(N阶贝塞尔曲线生成器)
2018/01/10 HTML / CSS
html5 移动端视频video的android兼容(去除播放控件、全屏)
2020/03/26 HTML / CSS
AOP的定义以及作用
2013/09/08 面试题
医院护士求职自荐信格式
2013/09/21 职场文书
导游词之杭州西湖
2019/09/19 职场文书
Nginx快速入门教程
2021/03/31 Servers
Go语言 go程释放操作(退出/销毁)
2021/04/30 Golang
十大最强电系宝可梦,阿尔宙斯电系之一,第七被称为雷神
2022/03/18 日漫
【海涛七七解说】DCG第二周:DK VS 天禄
2022/04/01 DOTA
特别篇动画《总之就是非常可爱 ~制服~》PV公开,2022年夏季播出
2022/04/04 日漫