在此之前我们先熟悉几个概念
- 目标元素 :当一个事件发生时 —— 发生该事件的嵌套最深的元素被标记为“目标元素”(
event.target) target.addEventListener(type, listener, options)方法type是表示监听事件类型的字符串。listener当所监听的事件类型触发时所执行事件options一个指定有关listener属性的可选参数对象
event.stopPropagation()来停止事件event.stopImmediatePropagation()可以用于停止冒泡,并阻止当前元素上的处理程序运行
有以下一个例子
<div id="parent">
<div id="child">
<div id="child1"></div>
</div>
</div>
事件冒泡(bubbling)
当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。
就像是气泡从水底一直冒到顶部,事件冒泡也是,实例代码的事件运行顺序从 child1 -> child -> parent,直到最后一直运行到document上的事件
<style>
body * {
margin: 10px;
border: 1px solid blue;
}
</style>
<form onclick="alert('form')">
FORM
<div onclick="alert('div')">
DIV
<p onclick="alert('p')">P</p>
</div>
</form>
点击内部的<p>会首先运行 onclick:
- 在该
<p>上的。 - 然后是外部
<div>上的。 - 然后是外部
<form>上的。 - 以此类推,直到最后的
document对象。
停止冒泡
在冒泡的这一路上会调用所有事件处理程序,但是任意处理程序都可以决定事件已经被完全处理,并停止冒泡
用于停止冒泡的方法就是event.stopPropagation()
<body onclick="alert(`the bubbling doesn't reach here`)">
<button onclick="event.stopPropagation()">Click me</button>
</body>
如果你点击<button>,这里的body.onclick不会工作,因为已经在 button 这里停止冒泡了
事件捕获
事件捕获恰好是与事件冒泡是相反的,事件冒泡是从下往上执行,捕获则是从上往下执行
这也就引出 dom 的事件的三个阶段:
- 事件捕获 :事件(从 Window)向下走近元素
- 目标阶段 :事件到达目标元素
- 事件冒泡 :事件从元素上开始冒泡
为了捕获阶段捕获事件,我们可以用target.addEventListener(type, listener, options)方法来捕获,可以将第三个参数options设置成 true
<form>
FORM
<div>
DIV
<p>P</p>
</div>
</form>
<script>
for (let elem of document.querySelectorAll('*')) {
elem.addEventListener(
'click',
(e) => alert(`Capturing: ${elem.tagName}`),
true
)
elem.addEventListener('click', (e) => alert(`Bubbling: ${elem.tagName}`))
}
</script>
这一段代码可以可以简单的表示出来事件的三个阶段,为每个元素都设置了事件处理程序,如果点击了<p>,则执行顺序是:
- HTML → BODY → FORM → DIV(捕获阶段第一个监听器)
- P(目标阶段,触发两次,因为我们设置了两个监听器:捕获和冒泡)
- DIV → FORM → BODY → HTML(冒泡阶段,第二个监听器)
当然我们也可以**让事件先冒泡后捕获, **对于同一个事件我们可以监听捕获和冒泡,当捕获事件执行时,我们可以暂缓执行,去先执行冒泡,在执行捕获
下面的例子用 setTimeout 延迟执行
<div id="parent">
<div id="child">
<div id="child1"></div>
</div>
</div>
<script>
const child1 = document.getElementById("child1")
child1.addEventListener("click", () => {
console.log("我是冒泡")
}, false)
child1.addEventListener('click',()=>{
setTimeout(()=>{
console.log("我是捕获")
},100)
},true)
</script>
事件委托
事件委托,通俗来说就是将元素的事件委托给它的父级或者更外级元素处理
<ul id="list">
<li id="child1">1</li>
<li id="child2">2</li>
<li id="child3">3</li>
</ul>
那这个例子来说就是如果需求是点击每个元素都会弹出不同的窗口展示不同的内容,那么我们第一次可能会想到给每一个元素都绑定上一个事件,但是考虑到事件冒泡与捕获,给每个元素都绑定的方法会消耗内存,并且每当有新元素添加时都会要重新再绑定
循环给每个元素绑定事件,当单击元素时会打印相应的内容
<ul id="list">
<li id="child1">1</li>
<li id="child2">2</li>
<li id="child3">3</li>
</ul>
<script>
const list = document.getElementById('list')
const li = list.getElementsByTagName('li')
for (let i in li) {
li[i].onclick = function (e) {
console.log(e.target.innerHTML)
}
}
</script>
分析一下 dom 操作:首先要找到 ul,然后遍历 li,然后点击 li 的时候,又要找一次目标的 li 的位置,才能执行最后的操作,每次点击都要找一次 li
但是当我们用事件委托方式做是怎样呢
<ul id="list">
<li id="child1">1</li>
<li id="child2">2</li>
<li id="child3">3</li>
</ul>
<script>
const list = document.getElementById('list')
list.onclick = function (e) {
if (e.target.nodeName.toLowerCase() == 'li') {
// 判断一下只有在时li时才会打印
console.log(e.target.innerHTML)
}
}
</script>
这样=直有在点击li的时候才会调用,且每次只执行一次 dom 操作
但是当我们想要给每一个不同的li绑定不同的事件呢,别忘了我们有一个特性是 data-xxx(data-xxx 全局属性 是一类被称为自定义数据属性的属性,它赋予我们在所有 HTML 元素上嵌入自定义数据属性的能力)
举个栗子,我们有三个按钮执行不同的事件,我们就可以用上 data-xxx
<div id="list">
<button data-counter="save">save click</button>
<button data-counter="load">save load</button>
<button data-counter="del">save delete</button>
</div>
<script>
const ele = document.getElementById('list')
ele.save = () => {
console.log('i am save')
}
ele.del = () => {
console.log('i am del')
}
ele.load = () => {
console.log('i am load')
}
ele.onclick = function (e) {
const counter = e.target.dataset.counter
if (counter) ele[counter]()
}
</script>
🆗,总结下大概流程
- 在容器(container)上放一个处理程序
- 在处理程序中 —— 检查源元素 event.target
- 如果事件发生在我们感兴趣的元素内,那么处理该事件
那事件委托其优缺点呢?
优点:
- 简化初始化并节省内存:无需添加许多处理程序
- 更少的代码:添加或移除元素时,无需添加/移除处理程序
- DOM 修改 :我们可以使用 innerHTML 等,来批量添加/移除元素
缺点:
- 首先,事件必须冒泡,而有些事件不会冒泡;此外,低级别的处理程序不应该使用 event.stopPropagation()
- 其次,委托可能会增加 CPU 负载,因为容器级别的处理程序会对容器中任意位置的事件做出反应,而不管我们是否对该事件感兴趣,但是,通常负载可以忽略不计,所以我们不考虑它