DOM事件机制
引入
从下面代码引入我们今天的话题
<div class="爷爷">
<div class="爸爸">
<div class="儿子">
文字
</div>
</div>
</div>
复制代码
对于上述代码,给三个 div 分别添加事件监听 fnYe、fnBa、fnEr。
提问 1:点击了谁?
- 点击文字,算不算点击爷爷
- 点击文字,算不算点击爸爸
- 点击文字,算不算点击儿子
答案:都算。
提问 2:调用顺序
点击文字,最先调用 fnYe/fnBa/fnEr 中的哪一个函数?
答案:都行。IE 认为先调用 fnEr,网景认为先调用 fnYe。
W3C 事件模型
直到 2002 年,W3C 发布了标准。规定了浏览器应该同事支持两种调用顺序,首先按照爷爷=>爸爸=>儿子的顺序看有没有函数监听,然后按照儿子=>爸爸=>爷爷的顺序看有没有函数监听,有监听函数就调用,并且提供事件信息,没有就直接跳过。
先捕获再冒泡,注意 e 对象被传给所有监听函数,事件结束后,e 对象就不存在了。
那么这里还存在一个问题:难道 fnYe、fnBa、fnEr 都要调用两次了,当然不是的,这里由开发者自己选择是吧 fnYe 放在捕获阶段还是冒泡阶段
DOM 事件机制分为捕获阶段和冒泡阶段。
那么我们上边提到的捕获和冒泡是什么呢?
事件捕获:简单来说就是从外向内找监听函数。例如上述代码中,事件捕获的监听顺序就是 fnYe>fnBa>fnEr。
事件冒泡:简单来说就是从内向外找监听函数。例如上述代码中,事件冒泡的监听顺序就是 fnEr>fnBa>fnYe。
DOM 事件机制的示意图如下图所示,可以帮助我们简单理解捕获和冒泡。
我们再来讨论一下捕获和冒泡的顺序,按照 WC3 的标准,我们可以得出先捕获后冒泡的这样一个顺序。但是,有一个特例,不是这样的。
当只有一个元素被监听的时候,(不考虑父子同时被监听),并且 fn 分别在捕获阶段和冒泡阶段同时监听 click 事件,这种情况下,用户点击的元素就是开发者监听的元素,谁先监听那么谁就先执行。下面两张图可供参考。
取消冒泡
知道了捕获和冒泡的顺序,我们再来看看如何取消冒泡(捕获是不能取消的)。
使用 e.stopPropagetion 就可以中断冒泡。但是有些事件是不能阻止默认动作的,例如 scroll event(滚轮事件)。MDN 搜索 scroll event 可以看到下图的内容,Bubbles 是指该事件是否冒泡,所有的冒泡都可以取消。Cancelable 是指开发者是否可以阻止默认事件,与冒泡无关。
事件委托
事件委托:通过监听一个祖先元素,同时操作多个后代。
场景一:
我们要给 100 个按钮添加点击事件,
怎么办?
最笨的办法:直接给 100 个按钮都 addEventListener。
有了事件委托后:监听这 100 个按钮的爸爸,等冒泡的时候,判断 target 是不是这 100 个按钮中的一个
场景二:
我们要监听目前不存在的元素的点击事件,咋办?
有了事件委托:监听祖先,等到冒泡时,判断点击的元素是不是我想要监听的元素。
要求:写出这样一个函数 on('click','#testDiv','li',fn)
,当用户点击 #testDiv 里的 li 元素时,调用 fn 函数,并且要用到事件委托。
html 代码如下:
<div id="test">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</div>
复制代码
JS 代码实现如下:
test.addEventListener('click',(e)=>{
const t = e.target
if(t.tagName.toLowerCase()==='li'){ //判断被操元素tagName是不是li
console.log('li 被点击了')
console.log('点击内容是'+t.textContent)
}
})
复制代码
上述代码实现思路:
- 首先监听父元素。
- 然后根据浏览器传进去的事件信息,拿到当前点击元素。
- 再判断当前点击元素是不是li元素,如果是,就 console.log(‘用户点击 li 标签’)
所以,我们就可以封装一个事件委托函数。
on('click', '#test', 'li', ()=>{
console.log('用户点击了 li')
})
function on(eventType, element, selector, fn){
if(!(element instanceof Element)){ //判断是不是element
element = document.querySelector(element)
}
element.addEventListener(eventType,(e)=>{
const t = e.target
if(t.matches(selector)){
fn(e)
console.log('点击内容是'+t.textContent)
}
})
}
复制代码
但是,上面这个委托函数还是存在问题的。
如果,给 li 添加 span,代码如下:
<ul id="test">
<li>
<span>1</span>
</li>
<li>
<span>2</span>
</li>
<li>
<span>3</span>
</li>
<li>
<span>4</span>
</li>
</ul>
复制代码
此时,上面的委托函数就不适用了,因为此时的被操的 e.target 是 span 而不是 li 了,就必须使用下面的委托函数。
function on(element, eventType, selector, fn) {
element.addEventListener(eventType, e => {
let el = e.target
while (!el.matches(selector)) {
if (element === el) {
el = null
break
}
el = el.parentNode
}
el && fn.call(el, e, el)
})
return element
}
复制代码