学这个的时候,这东西把我搞得很头晕。于是决定写一篇Blog出来把这玩意说清楚,让和我一样在这个坑里打滚的人不那么晕菜。尽量照顾到每个细节,标准是让自己再看的时候不管忘记了多少知识点都能看得懂。
那么,开始吧。
基础知识
1.鼠标点击。
我的鼠标左键点击了网页,然后网页获取到了我的点击事件,调取了DOMAPI——真的是这样吗?
不,不是这样的。鼠标首先应该是系统级的API,所以最先知情的应该是系统。系统得知你点击了鼠标,并把这个事件传递给浏览器,然后浏览器才会通过DOMAPI通知网页。这很重要。
2.事件绑定
DOM的事件元素绑定其实没啥好说的,无非就是绑定的方法不同。一种是直接绑定,就是xx.onclick这样的。简单粗暴,DOM level0就支持。等等,什么是DOM level0?这又引出了一个隐藏知识点。DOM level0 指的是DOM level1事件之前即支持的事件,DOM level1时间基准为W3C制定第一个标准。另外一种,就是DOM level2新增的事件监听,见下文。
3.child元素和parent元素的通知问题。
根据1,那么我点击了child元素并且child元素被监听,那么就会出现一个通知顺序的问题。是父元素先通知,还是子元素先通知?那么,就顺理成章的进入了下一个介绍:
捕获和冒泡
要说捕获和冒泡,得先介绍事件监听。事件监听和绑定其实差不了太多,但是新增了一个特点,就是无论监听多少次,都不会覆盖掉前面的事件。讲白了就是事件绑定plus。而捕获和冒泡其实本质上只是Child和Parent通知的两种顺序。捕获指的是parent先通知,child后通知,而冒泡则相反.
捕获只有在早期的浏览器中才使用,第一个尝试冒泡的,居然是现在被批判为封建保守的IE。有因有果,这都是有故事的啊~
一大堆概念解释:
事件捕获:当某个元素触发某个事件(如onclick),顶层对象document就会发出一个事件流,随着DOM树的节点向目标元素节点流去,直到到达事件真正发生的目标元素。在这个过程中,事件相应的监听函数是不会被触发的。
事件目标:当到达目标元素之后,执行目标元素该事件相应的处理函数。如果没有绑定监听函数,那就不执行。
事件冒泡:从目标元素开始,往顶层元素传播。途中如果有节点绑定了相应的事件处理函数,这些函数都会被一次触发。
有些情况下,捕获/冒泡并不被我们所需要,比如调试。那么我们可以使用e.stopPropagation()(Firefox)或者e.cancelBubble=true(IE)来阻止。e是什么?DOM Event。当然,我们也能通过改变addEventListener的第三个参数改变事件的执行顺序。(false为冒泡阶段执行,true为捕获阶段执行,默认为false)我想,大概永远都用不到True了吧....
事件委托(事件代理)
介绍完上面的,事件委托是时候登场了。事件委托简单说起来就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
网上我找了很多篇博客,基本都是用这个例子,我就直接抄过来了:
有三个同事预计会在周一收到快递。为签收快递,有两种办法:一是三个人在公司门口等快递;二是委托给前台MM代为签收。现实当中,我们大都采用委托的方案,前台MM收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优势,那就是即使公司里来了新员工(不管多少),前台MM也会在收到寄给新员工的快递后核实并代为签收。
嗯,我大概明白是啥意思了。来个例子加深一下印象吧。
<ul id="ul1">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
window.onload = function(){
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName('li');
for(var i=0;i<aLi.length;i++){
aLi[i].onclick = function(){
alert(123);
}
}
}
这是一段传统的的DOM操作遍历li实现事件操作的代码。有用,但是很蠢。遍历每一个li?真的有这个必要吗?
聪明的人已经想到了,那么我监听ul不就好了吗!bingo,那么用事件委托监听ul会怎么样呢?
window.onload = function(){
var oUl = document.getElementById("ul1");
oUl.onclick = function(){
alert(123);
}
嗯,点击ul确实已经实现了功能。但是出现了一个bug.当我们点击li之外,ul之内,事件一样触发了。因为事件是绑定在ul上的啊!也许通过控制ul的样式能够解决这个问题,但是并不是一个好办法。嗯,其实写个判断就能够解决了。这里有个需要解释的e.什么是e?DOM Event.它提供了一个属性叫target,可以返回事件的目标节点。有个小坑:webkit等浏览器一般是使用e.target,而IE浏览器要用event.srcElement.而且这个target只是获取节点位置,我们还需要用nodeName获取标签名,并转化为小写做比较。
ul.addEventListener('click', function(e) {
// 检查事件源e.targe是否为Li
if (e.target && e.target.nodeName.toUpperCase == "LI") {
// 真正的处理过程在这里
console.log("123");
}
}
这样就只有点击li会触发事件了,并且只执行一次dom操作。绝大多数的blog也是这么写的。
但是,它有bug!
如果第一个li外面套了一个span呢?
<ul id="ul1">
<span><li>111</li></span>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
再次点击第一个li,它居然不触发!为什么?console大法一下,发现我们点击的居然是span.那么这个循坏就错的彻彻底底了。但是其实做个小小的修改就行了。
ul.addEventListener('click', function() {
let el = e.target
while (el && !el.matches(selector)) {
el = el.parentNode
if (element === el) {
el = null
}
}
if (el) {
console.log('123')
}
}
OK,写写抄抄终于总结完了,自己算是弄懂了,下次有机会就用起来试试~