捕获、冒泡和事件委托
一、DOM和DOM树
DOM
概念:DOM(Document Object Model —— 文档对象模型)是一种将 HTML、XML 文档结构转化为树状对象的编程接口。它把文档里的元素、文本、属性等都变成可操作的 “节点对象”,让编程语言(比如 JavaScript)能方便地访问、修改文档的内容、结构和样式,是实现网页动态交互的基础
作用:开发网页内容特效和实现用户交互
DOM树
概念:DOM 树是 DOM(文档对象模型)对 HTML 或 XML 文档结构的一种树状可视化表示。它将文档中的所有内容(元素、文本、属性、注释等)抽象为 “节点”,并按照它们在文档中的层级关系组织成类似树的结构
DOM 树的核心特点:
根节点:整个树的起点是
document对象(代表整个文档),其直接子节点通常是 HTML 文档的<html>元素层级关系:每个节点有明确的父子、兄弟关系。例如,
<html>是<head>和<body>的父节点,而<head>和<body>互为兄弟节点节点类型:树中包含多种节点(如元素节点
<div>、文本节点 “Hello”、属性节点class="box"等),共同构成文档的完整结构
作用:DOM树直观的体现了标签与标签之间的关系
总结
DOM:为我们提供了一套专门用来操作网页内容的接口,这时候他是一种抽象的模型概念,看不见摸不着。它本质是一套规范和接口(比如 “如何获取元素” “如何修改文本” 的规则),定义了操作文档的方式,但本身不是一个可见的实体,更像是一种 “协议” 或 “逻辑框架”
DOM树:将这个模型概念给具体化可视化了,变成我们能看见画出来的一种树状结构。是对 DOM 模型的直观呈现,通过树状结构把文档的层级关系(父子、兄弟节点)画出来,让抽象的 “节点关系” 变成了能看懂的图形,帮助理解 DOM 的结构逻辑
一张概览图
二、事件流
为什么要先介绍DOM和DOM树:
- DOM、DOM 树跟事件流有很紧密的关联,简单说:DOM 是基础接口,DOM 树是结构载体,事件流是基于 DOM 树的事件传播规则。
什么是事件流:
- 事件完整执行过程中的流动路径
用一个比喻理解:
把 DOM 树看作一棵 “网页树”,树上的每个枝桠、每片叶子(对应 DOM 节点,如按钮、文本)都是可能被 “触碰”(触发事件,如点击)的对象
当我们触碰一片叶子(比如点击一个按钮),这个 “触碰信号” 不会直接就到达被触碰的目标叶子上,也不会到达之后一直就停留再目标叶子上 —— 它会先从树根(document)出发,沿着树干、树枝向下 “找”,一路 “找” 到被触碰的叶子上;等到达叶子(目标节点)后,信号又会沿着原来的树枝、树干向上 “返”,一路回到树根。这个 “从根向下找、再从目标向上返” 的完整信号传播路径和规则,就是事件流
事件流有三个阶段
事件捕获
目标阶段
- 事件到达实际触发事件的目标节点(比如被点击的按钮),触发该节点上绑定的事件处理函数的阶段
事件冒泡
通过上面的比喻,简单来说,捕获阶段是从父到子,冒泡阶段是从子到父
一个流程图
事件捕获
概念:事件从DOM树的根节点(document)向下逐层寻找目标节点的过程(一个由上到下的过程)。如果这个从上到下的过程中遇到的节点有绑定与你的触发动作(我点击了一下)相同的事件监听(有一个点击的事件监听),就会先执行该节点上对应的事件处理函数,等所有上层节点的事件执行完,才到目标节点本身
一个比喻理解:
- 把DOM 树当成一棵大树,我们要触碰的叶子(比如点击按钮,即目标节点)是最终要找的 “目的地”。事件捕获就像 “信号从树根出发,沿着树干、大枝桠、小枝桠,一步步向下找到那片被触碰的叶子”—— 沿途每经过一个节点(树干、大枝桠、小枝桠),如果这个节点绑定了 “捕获阶段” 的事件监听,就会先触发这个监听函数,再继续往下走,直到抵达目标叶子
注意:事件捕获需要写对应代码才能看到效果
代码:
DOM元素.addEventListener(事件类型,事件处理函数,是否使用捕获机制)代码参数说明:
addEventListener第三个参数传入true代表是捕获阶段触发(较少使用)- 若传入
false代表冒泡阶段触发,默认就是false - 若是用 L0 级事件监听,则只有冒泡阶段,没有捕获
<!--HTML标签内直接绑定--><button onclick="alert('点击了')">按钮</button>// JS中通过on+事件名绑定constbtn=document.querySelector('button');btn.onclick=function(){alert('L0级点击事件触发');};一个事件捕获的代码案例:
<style>#grandpa{margin:100px auto;width:300px;height:300px;background-color:cadetblue;}#father{width:200px;height:200px;background-color:darkseagreen;}#son{width:100px;height:100px;background-color:lightblue;}</style><!--三层嵌套:爷爷 → 爸爸 → 儿子--><div id="grandpa">爷爷<div id="father">爸爸<div id="son">点击我(儿子)</div></div></div><script>// 给三层都绑定捕获阶段的点击事件(第三个参数为true)grandpa.addEventListener('click',()=>{alert('爷爷 - 捕获阶段');},true);father.addEventListener('click',()=>{alert('爸爸 - 捕获阶段');},true);son.addEventListener('click',()=>{alert('儿子 - 捕获阶段');},true);</script>
事件冒泡
概念:事件从触发的的目标节点开始,向上逐层 “扩散” 回到DOM树的根节点的过程(由下向上的过程)。这个从下到上扩散回根节点的过程中,沿途如果有节点绑定了与你触发目标节点的触发动作相同的监听事件,就会向上依次触发这些监听函数,知道回到根节点
一个比喻理解:
- 假设你点击了一片叶子(目标节点,比如一个按钮),事件冒泡就像 “叶子被点击的信号,沿着树枝、树干一路向上传到树根”—— 沿途向上经过的每个节点(比如按钮的父 div、父 body、甚至 html),如果绑定了 “冒泡阶段” 的点击的事件监听,就会依次触发这些监听函数,直到信号传到最顶端的
document
- 假设你点击了一片叶子(目标节点,比如一个按钮),事件冒泡就像 “叶子被点击的信号,沿着树枝、树干一路向上传到树根”—— 沿途向上经过的每个节点(比如按钮的父 div、父 body、甚至 html),如果绑定了 “冒泡阶段” 的点击的事件监听,就会依次触发这些监听函数,直到信号传到最顶端的
简单理解:当一个元素触发事件后,会依次向上触发所有父级的同名事件
注意:事件冒泡在大多数常用事件(如click、input、mouseover)里面都是是默认存在的,L1 级、L2 级事件监听第三个参数是 false,或者默认都是冒泡,但是也有少数事件(如focus、blur、scroll)没有冒泡特性
一个事件冒泡的代码案例:
grandpa.addEventListener('click',()=>{alert('爷爷 - 冒泡阶段');});father.addEventListener('click',()=>{alert('爸爸 - 冒泡阶段');});son.addEventListener('click',()=>{alert('儿子 - 冒泡阶段');});
一个小小的问题
// 以上面的案例里面的其中一个事件监听为例son.addEventListener('click',()=>{alert('儿子 - 捕获阶段');});现在上面这个案例里面第三个参数为默认(
false),即现在这个事件监听它监听的是事件冒泡的过程,那么,问题来了,这个事件流里面事件捕获阶段还存在吗?- 答案是存在,只是此时绑定的事件处理函数不会在捕获阶段被触发,只会在目标阶段和冒泡阶段响应触发
为什么呢?
事件流的三个阶段(捕获 → 目标 → 冒泡)是DOM 规范规定的固定流程,只要事件被触发,这三个阶段就一定会完整执行,与事件绑定方式(是否监听捕获阶段)无关
addEventListener的第三个参数(useCapture)的作用是决定当前事件处理函数在哪个阶段响应:当
useCapture为true时:函数在捕获阶段触发(从根到目标的过程中)当
useCapture为false或省略时:函数在冒泡阶段触发(到达目标时,或从目标向根传播时)。目标阶段不论
useCapture参数是true还是false都会触发
一个关于触发顺序的例子
grandpa.addEventListener('click',()=>{alert('爷爷');},true);father.addEventListener('click',()=>{alert('爸爸');});son.addEventListener('click',()=>{alert('儿子');},true);
控制事件行为的两种方法
理解了捕获和冒泡的原理后,为了避免“意外触发”或实现复杂的事件逻辑,我们将介绍一个阻止事件传播的方法。
以及另一个阻止默认行为方法,因为经常会与阻止事件传播组合搭配使用,所以我们放在这里一并介绍
阻止事件传播:stopPropagation()
event.stopPropagation()方法可以阻止事件继续向下或向上传播,无论是在捕获阶段还是冒泡阶段调用,都能中断后续的传播过程两个小问题:
ev.stopPropagation()会阻止目标元素事件监听的执行吗?- 不会,前面的定义说过,只是会中断捕获和冒泡的后续传播,具体效果看下面例子的效果
ev.stopPropagation()会影响该元素其他事件的后续传播吗?- 也不会,每次事件触发都是一个独立的事件对象(
ev),调用stopPropagation()仅作用于当前这个事件实例,不会影响未来触发的其他事件(即使是同一类型)。具体效果看下面例子的效果:
- 也不会,每次事件触发都是一个独立的事件对象(
一个具体例子:
// 思考:// 以上三个事件监听如果都改为默认的冒泡阶段监听时,ev.stopPropagation()这句话在第二个事件监听里面出现和第三个事件监听里面出现,结果有什么不同?grandpa.addEventListener('click',()=>{alert('爷爷');},true);father.addEventListener('click',(ev)=>{ev.stopPropagation()alert('爸爸');},true);son.addEventListener('click',(ev)=>{// ev.stopPropagation();alert('儿子');},true);grandpa.addEventListener('mouseover',()=>{alert('爷爷(鼠标移入事件出现)');},true);father.addEventListener('mouseover',(ev)=>{alert('爸爸(鼠标移入事件出现)');},true);son.addEventListener('mouseover',(ev)=>{// ev.stopPropagation();alert('儿子(鼠标移入事件出现)');},true);
阻止默认行为:preventDefault()
为什么需要?
- 很多事件都有浏览器预设的默认行为,比如点击链接会跳转、提交按钮会提交表单。有时候我们不需要浏览器预设的默认行为,我们想要自定义一些事件的默认行为,就需要先阻止浏览器预设的默认行为,再设置我们自己的。所以介绍的这个
event.preventDefault()方法就可以阻止这些默认行为
- 很多事件都有浏览器预设的默认行为,比如点击链接会跳转、提交按钮会提交表单。有时候我们不需要浏览器预设的默认行为,我们想要自定义一些事件的默认行为,就需要先阻止浏览器预设的默认行为,再设置我们自己的。所以介绍的这个
与阻止事件传播方法的区别
- 不会影响事件的传播过程(这是与
stopPropagation()的核心区别(作用对象不同))
- 不会影响事件的传播过程(这是与
一个具体例子
<body><a href="https://example.com"id="link">点击我</a><form id="form"action="/submit"><button type="submit">提交</button></form><script>// 阻止链接跳转constlink=document.getElementById('link');document.querySelector('div').addEventListener('click',function(){console.log('父盒子被触发');})link.addEventListener('click',(e)=>{// 取消默认跳转行为e.preventDefault();console.log('链接被点击,但不跳转');});// 阻止表单提交constform=document.getElementById('form');form.addEventListener('submit',(e)=>{// 取消默认提交刷新e.preventDefault();console.log('表单提交被拦截,将异步处理');});</script></body>
stopPropagation()与preventDefault()核心区别总结
| 特性 | stopPropagation() | preventDefault() |
|---|---|---|
| 作用对象 | 事件的传播过程(捕获 / 冒泡) | 事件的默认行为(浏览器预设) |
| 是否影响传播 | 是(中断传播链) | 否(传播正常进行) |
| 是否影响默认行为 | 否(默认行为仍会执行) | 是(取消默认行为) |
| 典型使用场景 | 阻止父子元素事件相互干扰 | 自定义元素行为(如链接、表单) |
三、事件委托
在说事件委托之前,有个问题,在前面提到捕获和冒泡的比喻里面说的传递信号,大家知道这个传递的信号在事件捕获和冒泡里面指的是什么吗?
本质是包含了 “触发节点信息” 在内的「事件对象(event)」—— 它装着整个事件的完整信息,是事件传播时的 “信息载体”
这个完整信息包括:
谁是触发者?(
event.target,就是被点的 li,即 “触发节点信息”)事件类型是什么?(
event.type,比如是“click”点击事件)事件是在哪个阶段传播的?(
event.eventPhase,捕获 / 目标 / 冒泡)点击时鼠标在哪个位置?(
event.clientX/event.clientY)甚至能让信号停止传播(
event.stopPropagation())、阻止默认行为(event.preventDefault())
事件委托(也叫事件代理)是一种利用事件冒泡特性实现的高效事件处理技巧 —— 简单说,就是不给子节点单独绑定事件,而是把事件绑定在它们的父节点上,由父节点 “代理” 处理所有子节点的事件
核心逻辑:
- 因为事件会冒泡,子节点触发的事件,最终会传到父节点上。父节点监听到了事件对像携带的事件类型与父节点绑定事件类型一致后,就执行父节点对应的事件处理函数,通过事件对象(
event)的target属性,判断是哪个子节点触发了事件,再针对性处理
- 因为事件会冒泡,子节点触发的事件,最终会传到父节点上。父节点监听到了事件对像携带的事件类型与父节点绑定事件类型一致后,就执行父节点对应的事件处理函数,通过事件对象(
一个例子:
<style>#list{margin:100px auto;width:200px;background-color:antiquewhite;}#list li{margin-bottom:10px;width:100%;background-color:lightpink;}</style><!--父节点:ul;子节点:多个li--><ul id="list"><li>列表项1</li><li>列表项2</li><li>列表项3</li></ul><script>// 只给父节点ul绑定一次点击事件(代理所有li的事件)document.getElementById('list').addEventListener('click',function(ev){// e.target 就是触发事件的子节点(被点击的li)// 确保点击的是 li 节点if(ev.target.tagName==='LI'){alert(`点击了:${ev.target.textContent}`);}});</script>