一. 引入
<div class=爷爷>
<div class=爸爸>
<div class=儿子>
文字
</div>
</div>
<div>
上面代码是一个点击事件的HTML代码,层级关系为 .爷爷 > .爸爸 > .儿子,现给三个 div 分别添加事件监听fnYe / fnBa / fnEr。
提问1(点击了谁):点击文字,算不算点击儿子?算不算点击爸爸?算不算点击爷爷?
答:都算。
提问2(调用顺序):点击文字,最先调用fnYe / fnBa / fnEr中的哪一个函数?
答:都行。
二. DOM事件模型
1. 事件捕获和事件冒泡
当一个事件发生在具有父元素的元素上时,浏览器会运行两个不同的阶段 - 捕获阶段和冒泡阶段。
- 事件捕获:从外向内找监听函数(爷爷=>爸爸=>儿子)
- 事件冒泡:从内向外找监听函数(儿子=>爸爸=>爷爷)
捕获:当用户点击按钮,浏览器会从 window 从上往下遍历至用户点击的按钮,逐个触发事件处理函数。
冒泡:浏览器从用户点击的按钮从下往上遍历至 window,逐个触发事件处理函数。
2. W3C 事件模型/事件机制
对于引入描述的点击事件的调用顺序,IE5 认为先调用fnEr,网景认为先调用fnYe,后来W3C 发布标准,规定浏览器应该同时支持两种调用顺序。
首先按爷爷=>爸爸=>儿子(先捕获)顺序看有没有函数监听,然后按儿子=>爸爸=>爷爷(再冒泡)顺序看有没有函数监听。有监听函数就调用,并提供事件信息,没有就跳过。
对每个事件先捕获再冒泡,这就是 W3C 事件模型/事件机制。
3. addEventListener
关于点击事件例子的疑问:那岂不是 fnYe / fnBa / fnEr 都调用两次?
答:非也!开发者可以使用addEventListener()自己选择把函数监听放在捕获阶段还是放在冒泡阶段。addEventListener是一个事件绑定 API,其语法如下:
target.addEventListener('click', fn, bool)
- 如果 bool 不传或为 falsy 值
fn 走冒泡,即当浏览器在冒泡阶段发现 target 有 fn 监听函数,就会调用 fn,并提供事件信息。
- 如果 bool 为 true
fn 走捕获,即当浏览器在捕获阶段发现 target 有 fn 监听函数,就会调用 fn,并提供事件信息。
4. target v.s. currentTarget
区别
- e.target - 用户操作的元素
- e.currentTarget - 开发者监听的元素
- this是e.currentTarget,但是不推荐使用
举例
div > span{文字},用户点击文字
- e.target 就是 span
- e.currentTarget 就是 div
5. 只有一个 div 被监听
只有一个 div 被监听时(不考虑父子同时被监听),fn 分别在捕获阶段和冒泡阶段监听 click 事件,此时用户点击的元素就是开发者监听的元素。
div.addEventListener('click', f1) //冒泡
div.addEventListener('click', f2, true) //捕获
提问:f1先执行还是f2先执行?如果把两行调换位置后,哪个先执行?
错误答案:f2先执行
正确答案:谁先监听谁先执行!
6. 取消冒泡——捕获不可取消,但冒泡可以
在现代浏览器中,默认情况下,所有事件处理程序都在冒泡阶段进行注册。
可以用e.stopPropagation()来中断冒泡,使浏览器不再向上走,一般用于封装某些独立的组件。
7. 阻止默认动作
使用e.preventDefault()来阻止默认动作,所有冒泡皆可取消,但是默认动作有的可以取消,有的不可取消,如 scroll 事件不可阻止默认动作。
三. 事件委托
事件委托就是,如果想要在大量子元素中单击任何一个都可以运行一段代码,那么可以将事件监听器设置在其祖先元素(如父元素、爷爷元素)上,并让子节点上发生的事件冒泡到祖先元素上,而不是每个子节点单独设置事件监听器。
事件委托的好处是:节约监听数量;可以监听动态生成的元素。
举例1:要给100个按钮添加点击事件,可以监听这100个按钮的父节点,等冒泡的时候判断 target 是不是这100个按钮中的一个。
div1.addEventListener('click', (e) => {
const t = e.target
if (t.tagName.toLowerCase() === 'button') {
console.log('button被点击了,内容是' + t.textContent)
}
})
举例2:要监听目前不存在的元素的点击事件,可以监听父节点,等点击的时候看看是不是想要监听的元素。
//HTML
<div id="div1"></div>
//JS
setTimeout(() => {
const button = document.createElement('button')
button.textContent = 'click 1'
div1.appendChild(button)
}, 1000) //一秒后创建 button
div1.addEventListener('click', (e) => {
const t = e.target
if (t.tagName.toLowerCase() === 'button') {
console.log('click button')
}
})
封装事件委托
写出这样一个函数on('click', '#testDiv', 'li', fn),当用户点击#testDiv里的li元素时,调用fn函数,要求用到事件委托
function on(eventType, element, selector, fn){
if(!(element instanceof Element)){
element = document.querySelector(element)
}
element.addEventListener(eventType, (e) => {
const t = e.target
if(t.matches(selector)){ //判断target是否匹配'li'
fn(e)
}
})
}
on('click', '#div1', 'button', ()=>{
console.log('button 被点击了')
})
matches(selector) 用于判断用户操作的元素是否与selector匹配。