事件:事件是用户或浏览器自身执行的某种动作,如 click、load 和 mouseover 都是事件的名字。响应某个事件的函数叫做事件处理函数/事件处理程序/事件句柄。如果想要绑定多个函数,则需要用到事件监听器。
1. 事件绑定的几种方式
1)在 HTML 标签中绑定
<input type="button" id="btn" onclick="fn()">
<!--或者-->
<input type="button" id="btn" onclick="alert(1)">
2)在 JS 代码中绑定
DOM0级事件处理程序
document.getElementById("btn").onclick = function(){
//......
};
说明:无法绑定多个事件处理函数,当绑定多个的时候,前面的会被覆盖,只有最后一个会生效。
IE 事件处理程序
document.getElementById("btn").attachEvent("onclick",function(){
//......
})
说明:
- 不属于 w3c 标准,仅 IE8 及以下支持该方法;
- 事件类型要加 on;
- 可以绑定多个事件处理函数,会按照绑定顺序一一执行
- 微软提出的,因此采用的机制也是微软提出的冒泡机制
DOM2级事件处理程序
document.getElementById("btn").addEventListener("click",function(){
//......
})
说明:
- 事件类型不加on;
- 第三个参数不设置的时候,默认为 false,即 useCapture = false,表示该函数会在事件流的冒泡阶段触发;
- 可以绑定多个事件处理函数,如果都是冒泡或者捕获,则会按照绑定顺序一一执行;如果一个是冒泡,一个是捕获,则捕获先于冒泡执行
2. DOM 事件流
1)什么是事件流?
当触发某个 DOM 元素上面的事件时,会产生一股事件流。事件流会有三个阶段:
- 捕获阶段:事件流会从根节点流向目标节点,并执行那些声明了在捕获阶段触发的同类型事件处理函数
- 目标阶段:事件流停在目标节点这里,并执行目标节点上的事件处理函数。这个阶段实际上通常看作是冒泡阶段的一部分
- 冒泡阶段:事件流会从目标节点再流向根节点,并执行那些声明了在冒泡阶段触发的同类型事件处理函数
2)如何获取事件流的相关信息?
在任意一个事件处理函数中,都可以拿到 event 参数,这个参数包含了和事件流相关的一些信息:
- type:用于获取事件类型
- target:实际触发事件的那个对象,或者说事件源对象,它是固定不变的
- currentTarget:当前事件流经过的对象,是动态变化的
- stopPropagation():阻止事件继续传播
- preventDefault():阻止事件的默认行为。移动端用的多
比如下面的代码:
<div id="son1">
<div id="son2">
<div id="son3"></div>
</div>
</div>
<script>
var son1 = document.getELementById("son1")
var son2 = document.getELementById("son2")
var son3 = document.getELementById("son3")
son1.addEventListener("click",log)
son2.addEventListener("click",log)
son3.addEventListener("click",log)
function log(e){
console.Log("target:" + e.target.id + " " + "currentTarget:" + e.currentTarget.id)
}
</script>
打印结果如下:
可以看到,点击最里面的 son3 后,target 一直不变。而由于冒泡,导致 currentTarget 是动态变化的。
3. 事件处理函数的执行顺序
单个元素绑定了多个事件处理函数,谁先执行呢?嵌套元素绑定了多个事件处理函数,谁先执行呢?
这里直接给出结论:
一个事件流可能经过的元素有两种,一种是目标元素(target),一种是事件流流过的元素(currentTarget)。对于事件流流过的元素,设置为 true 的事件处理函数,一定会在捕获阶段触发,设置为 false 的事件处理函数,一定会在冒泡阶段触发,这点不管在什么浏览器都一样;但对于目标元素则不同,在以前,该元素的事件处理函数的执行顺序,始终是按照绑定顺序执行的,指定 true 或者 false 没有任何影响;但对于现在的 Chrome 来说,指定 true 或者 false 则可以让该函数在捕获阶段或者冒泡阶段触发 —— 也就是说,目标元素可以真正参与到事件流的不同阶段中。
1)单个元素
先来看单个 DOM 元素的情况。以下面的代码为例:
<script>
window.onload = function(){
let outA = document.getElementById("outA");
outA.addEventListener('click',function(){console.log(1)},false)
outA.addEventListener('click',function(){console.log(2)},true)
outA.onclick = function(){console.log(5)}
outA.addEventListener('click',function(){console.log(3)},true)
outA.addEventListener('click',function(){console.log(4)},true)
};
</script>
<body>
<div id="outA"></div>
</body>
如果是旧版的 Chrome 或者现在的 Firefox:
那么无需理会事件捕获或者事件冒泡,事件处理函数是按照绑定顺序一一执行的
如果是 Chrome89 之后的版本:
那么声明了 true 的事件处理函数将在事件流的捕获阶段触发,而声明了 false 的或者 onclick 绑定的,则在冒泡阶段触发。问题来了,false 和 onclick 谁先执行呢?很简单,谁先绑定谁就先执行。
注意下面的代码。尽管 onclick 被覆盖了,但它始终是最先绑定的,这一点不会改变。所以执行的时候先打印 3 后打印 2。
outA.onclick = function(){console.log(1)}
outA.addEventListener('click',function(){console.log(2)},false)
outA.onclick = function(){console.log(3)}
2)嵌套元素
再来看存在嵌套的情况。注意这里所说的嵌套指的是 HTML 结构上的嵌套,不光是指视图上的。
以下面的代码为例:
<script>
window.onload = function(){
var outA = document.getElementById("outA");
var outB = document.getElementById("outB");
var outC = document.getElementById("outC");
outC.addEventListener("click",function(){console.log("target1");},false);
outC.addEventListener("click",function(){console.log("target2");},true);
outC.addEventListener("click",function(){console.log("target3");},true);
outC.addEventListener("click",function(){console.log("target4");},false);
outA.addEventListener("click",function(){console.log("bubble1");},false);
outB.onclick = function(){console.log(11111)}
outB.addEventListener("click",function(){console.log("bubble2");},false);
outA.onclick = function(){console.log(22222)}
outA.addEventListener("click",function(){console.log("capture1");},true);
outB.addEventListener("click",function(){console.log("capture2");},true);
};
</script>
<body>
<div id="outA">
<div id="outB">
<div id="outC"></div>
</div>
</div>
</body>
如果是旧版的 Chrome 或者现在的 Firefox:
事件流先经历捕获阶段,这个阶段按照 AB 的顺序执行那些声明了在捕获阶段触发的函数(capture1–>capture2);接着进入目标阶段,这个阶段无视目标节点的绑定类型是事件捕获还是事件冒泡,直接按照绑定顺序一一执行;然后进入冒泡阶段,这个阶段按照 BA 的顺序执行那些声明了在冒泡阶段触发的函数(11111–>bubble2–>bubble1–>22222)。注意 false 和 onclick 仍然没有明确的先后顺序,谁先注册谁就先执行。
如果是 Chrome89 之后的版本:
同样的,事件流先经历捕获阶段,这个阶段按照 ABC 的顺序执行那些声明了在捕获阶段触发的函数(capture1–>capture2–>target2–>target3);接着进入目标阶段,然后进入冒泡阶段,这个阶段按照 CBA 的顺序执行那些声明了在冒泡阶段触发的函数(target1–>target4–>11111–>bubble2–>bubble1–>22222)。注意 false 和 onclick 仍然没有明确的先后顺序,谁先注册谁就先执行。
4. 阻止事件冒泡和捕获
子元素上发生某个事件的时候,我们可能不希望执行父元素上绑定的事件处理函数,这时候可以选择阻止事件捕获和冒泡,避免没有意义的函数调用。
IE8 之前可以通过 window.event.cancelBubble = true
阻止事件的继续传播;IE9+/FF/Chrome 则是通过在事件处理函数里面调用 event.stopPropagation()
阻止事件的继续传播。
以下面代码为例:
<script>
window.onload = function(){
let outA = document.getElementById('outA')
let outB = document.getElementById('outB')
let outC = document.getElementById('outC')
outC.addEventListener('click',function(event){
console.log('target')
event.stopPropagation()
},false)
outA.addEventListener('click',function(){console.log('bubble')},false)
outA.addEventListener('click',function(){console.log('capture')},true)
}
</script>
<div id="outA">
<div id="outB">
<div id="outC"></div>
</div>
</div>
当点击 outC 的时候,打印出 capture–>target,不会打印出 bubble。
因为当事件流经过 outC 上的处理函数时,调用了 event.stopPropagation()
,阻止了事件继续传播到冒泡阶段。想要在哪个节点阻止传播,就在哪个节点的事件处理函数中调用 event.stopPropagation()
。
5. 事件代理/事件委托
1)概述
事件委托又叫事件代理。它本质上是基于事件冒泡的,通过只指定一个事件处理函数,来管理某一类型的所有事件。
我们可以用取快递的例子来理解这个东西:
假设:有三个同事预计会在周一收到快递。为签收快递,有两种办法:一是三个人在公司门口等快递;二是委托给前台代为签收。现实当中,我们大都采用委托的方案(公司也不会容忍那么多员工站在门口就为了等快递)。前台收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优势,那就是即使公司里来了新员工(不管多少),前台也会在收到寄给新员工的快递后核实并代为签收。
这里其实还有两层意思的:
第一,现在委托前台的同事是可以代为签收的,即程序中的现有的 dom 节点是有事件的;
第二,新员工也是可以被前台代为签收的,即程序中新添加的 dom 节点也是有事件的。
2)为什么要使用事件委托
简单来说,就是为了减少不必要的 dom 操作,优化性能
考虑这样一个场景:我们有 100 个 li,每个 li 都有相同的点击事件,可能我们会用 for 循环的方法,来遍历所有的 li,一一给它们添加事件处理函数,那这么做会存在什么影响呢?
每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就越大,自然性能就越差了。比如上面的 100 个 li,就要占用 100 个内存空间,如果是 1000 个,10000 个呢?但如果用事件委托,那么我们就可以只给所有 li 的某个公共父级绑定一个事件处理函数,虽然实际点击的是某个 li 子元素,但反正有冒泡机制,这个点击事件最终是会冒泡到达那个父元素的,在父元素的处理函数里,我们只要判断点击的是哪个子元素,并作相应的处理就可以了
3)事件委托的实际应用
需求一:不管点击哪个 li,都能打印 123
<ul id="ul">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
不使用事件委托,我们可能会这么实现:
window.onload = function(){
var ul = document.getElementById("ul")
var aLi = ul.getElementsByTagName('li')
for(var i = 0;i < aLi.length;i ++){
aLi[i].onclick = function(){
console.log(123)
}
}
}
使用事件委托,则只需要绑定一次:
window.onload = function(){
var ul = document.getElementById("ul")
ul.onclick = function(){
console.log(123)
}
}
不管是哪个 li 被点击 —— 由于冒泡机制,事件最终都会冒泡到 ul 上,并触发 ul 上的点击事件,弹出 123。
需求二:只有点击的子元素是 li 的时候,才会打印 123
<ul id="ul">
<li>111</li>
<div>222</div>
</ul>
之前我们说过,在任意一个事件处理函数中,可以通过 event.target
拿到此次事件的目标节点,或者说事件源节点,而这个节点提供了很多信息给我们。
只有事件源节点是 li 的时候 —— 只有点击的是 li 的时候,才打印 123:
window.onload = function(){
var ul = document.getElementById("ul");
ul.onclick = function(event){
var event = event || window.event
var target = event.target || event.srcElement
if(target.nodeName.toLowerCase() == 'li'){
console.log(123)
}
}
}
需求三:点击的子元素不同,执行不同的操作
<div id="box">
<input type="button" id="add" value="添加" />
<input type="button" id="remove" value="删除" />
<input type="button" id="move" value="移动" />
<input type="button" id="select" value="选择" />
</div>
这里依然可以通过 event.target
帮助判断:
window.onload = function(){
var oBox = document.getElementById("box");
oBox.onclick = function (event) {
var event = event || window.event
var target = event.target || event.srcElement
// 如果是 input 类型的子元素
if(target.nodeName.toLocaleLowerCase() == 'input'){
// 根据 input 的 id 不同,执行不同的操作
switch(target.id){
case 'add':
console.log('添加')
break
case 'remove':
console.log('删除')
break
case 'move':
console.log('移动')
break
case 'select':
console.log('选择')
break
}
}
}
}
如果是新增的节点,在它身上触发的事件可以被父元素代理吗?
这就是事件委托另一个强大的地方了。如果说我们是采用手动遍历,为所有子元素添加事件处理函数的方式,那么一旦新增节点,我们还得重复这个过程;而使用事件委托则不用,因为不管新增了什么节点,它都会是父节点的一个子节点,它触发的事件都可以被父节点代理。
4)注意事项
那什么样的事件可以用事件委托,什么样的事件不可以用呢?
- 诸如 click,mousedown,mouseup,keydown,keyup,keypress 等都是可以使用事件委托的。
- 诸如 mousemove 就不适合使用,因为每次都要计算它的位置,非常不好把控,至于 focus,blur 之类的就更不用说了,本身就没有冒泡的特性,自然就不能用事件委托了。