事件机制
事件机制拥有三个阶段,分别是:
事件捕获、处于目标和事件冒泡。
需要注意的是IE是没有事件捕获这一阶段,具体原因可自行查阅。
事件执行的时候,我们整个执行流程会先去捕获再进行冒泡。
- 捕获:
会从Document对象去向下传播事件,直到找到目标元素,才会停止传播。 - 冒泡:
会从目标元素向上传播事件,直到Document对象,才会停止传播。
捕获
Document -> DIV -> Target -> DIV -> Document冒泡
假设我们拥有下列的DOM结构,我们去依次分析事件捕获和事件冒泡,再去了解事件委托。
<ul>
<li>
<span>
<a></a>
</span>
</li>
</ul>
事件捕获
由网景最先提出,事件会从最外层开始触法,直到最具体的元素。
如上列DOM结构,假设我们的ul和span都绑定了点击事件,span没有脱离文档流被被ul包含,那么这时候点击span会先触发ul的点击事件,然后再触发span的点击事件。
ul.addEventListener('click',() => {
console.log('ul')
},true)
span.addEventListener('click',() => {
console.log('span')
},true)
// log执行顺序
// ul => span
可能我们不希望触法子元素的事件,这时候我们可以使用stopImmediatePropagation()方法来阻止事件捕获。
关于
stopImmediatePropagation方法的详细描述请查阅MDN
ul.addEventListener('click',(e) => {
e.stopImmediatePropagation()
console.log('ul')
},true)
// log 将不会打印 span
总得来说就是事件捕获就是: 优先触发父元素的事件再传递到子元素, 也就是 父 => 子。
事件冒泡
由微软提出,事件会从最内层开始触法,直到最外层的元素.
还是使用事件捕获时的例子来进行讲解, ul和span都绑定了点击事件, span没有脱离文档流, 那么这时候我们点击span的时候会和捕获时的执行顺序完全相反, 会优先触法span的点击事件,再触发ul的点击事件。
ul.addEventListener('click',() => {
console.log('ul')
})
span.addEventListener('click',(e) => {
console.log('span')
})
// log执行顺序
// span => ul
如果我们希望点击span不触法ul的事件,我们可以使用stopPropagation()方法来阻止事件冒泡。
关于
stopPropagation方法的详细描述请查阅MDN
span.addEventListener('click',(e) => {
e.stopPropagation()
console.log('span')
})
// log 将不会打印 ul
总得来说事件冒泡就是: 优先触法子元素的事件再传递到父元素,也就是 子 => 父这么一个流程。
TIP: 上面已经讲述了捕获和冒泡这两种机制和阻止传播的方法。由此可见这两种机制除了执行顺序的不一致,其他的都大差不差.
事件委托
事件委托又称为事件代理,简单来讲就是将元素一整块进行监听,是否触发了制定的事件。
ul.addEventListener('click',(e) => {
const targetName = e.target.nodeName
console.log(targetName)
switch (targetName) {
case "UL":
break;
...
default:
break;
}
})
// 根据每次点击到的元素打印: UL || LI || SPAN || A
上述例子,就是简单的事件委托用法,我们可以根据点击到的元素去执行对应的事件,而不需要每个元素都去绑定一个事件。
为什么我们只绑定了ul,却可以获取到li、span和a这些元素呢。因为我们使用事件委托将ul整个DOM结构都监听了,任何处于ul内部中的元素,都会触发点击事件。
通常我们会在拥有多个子元素且需要绑定相同事件的元素上去使用,例如:
<ul>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
我们需要做一个tab切换每个li都需要绑定点击事件,我们应该去使用事件委托而不是获取所有li去单独绑定。
// 不推荐
const liList = document.querySelectorAll("li")
for (let i = 0; i < liList.length; i++) {
const element = liList[i];
element.onClick = () => {
// TODO
}
}
// 推荐
const ul = document.querySelector("ul")
ul.addEventListener('click',(e) => {
if (e.target.nodeName === 'LI') {
// TODO
}
})
这么做的好处在于:
- 我们只需要绑定一次事件,提高效率
- 动态添加进去的元素也同样拥有事件
- 不需要管理多个函数 (这里指js引擎,而不是代码上)
- 减少了内存的消耗 (不需要额外获取dom和执行loop去挨个绑定事件)
- 减少了代码和DOM之间的关联 (只需要关注父元素,不需要关注内部元素)
- 修改DOM的时候不需要考虑删除事件 (修改dom的时候不需要思考之前绑定的事件没有删除是否会造成副作用)
TIP: 在
vue或react等框架中,我们需要注意在销毁组件时使用removeEventListener删除掉事件监听,否则多次绑定会执行多次,容易造成执行栈溢出。