目录
一、事件冒泡
1、什么是事件冒泡
当一个元素接收到事件的时候,会把接收到事件传递给自己的父级。
这里的传递仅仅是事件的传递,并不传递所绑定的事件。所以如果父级没有绑定事件函数,就算传递了事件,也不会有什么表现。但是事件确实传递了。
2、阻止事件冒泡的方式
标准的W3C方式:e.stopPropagation()
这里的stopPropagation是标准的事件对象的一个方法,直接调用即可。
非标准的IE方式:window.event.cancelBubble=true
这里的cancelBubble是IE事件对象的属性,设为true即可。
封装成函数:
function stopBubble(e){
// 如果提供了事件对象,则这是一个非IE浏览器
if (e && e.stopPropagation) {
// 支持W3C的stopPropagation()方法
e.stopPropagation()
} else {
// 否则 使用IE的方式阻止冒泡
window.event.cancelBubble = true
}
}
3、事件冒泡的优点
方便做事件委托 。
二、事件委托/事件代理
1、什么是事件委托
事件委托,也叫事件代理。
就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
2、为什么要用事件委托?
① 提高整体运行性能
一般来说,DOM 需要有事件处理程序,我们都会直接给它设事件处理程序就好了。那如果是很多的 DOM 需要添加事件处理呢?
比如我们有100个 li,每个li都有相同的 click 点击事件。可能我们会用for循环的方法,来遍历所有的 li,然后给它们添加事件,那这么做会存在什么影响呢?
在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。因为需要不断的与 DOM 节点进行交互,访问 DOM 的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间。这就是为什么性能优化的主要思想之一就是减少DOM操作的原因。
如果要用事件委托,就会将所有的操作放到 js 程序里面,与 DOM 的操作就只需要交互一次,这样就能大大的减少与 DOM 的交互次数,提高性能。
② 减少内存空间
每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就越大,自然性能就越差了。
比如上面的100个 li,就要占用100个内存空间,如果是1000个,10000个呢~~
如果用事件委托,那么我们就可以只对它的父级(如果只有一个父级)这一个对象进行操作,这样我们就需要一个内存空间就够了,是不是省了很多,自然性能就会更好。
3、事件委托的原理
事件委托是利用事件的冒泡原理来实现的。
事件冒泡就是事件从最深的节点开始,然后逐步向上传播事件。
举个例子:页面上有这么一个节点树,div>ul>li>a; 比如给最里面的a加一个click点击事件,那么这个事件就会一层一层的往外执行,执行顺序a>li>ul>div。
有这样一个机制,那么我们给最外面的div加点击事件。那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发。这就是事件委托,委托它们父级代为执行事件。
4、事件委托怎么实现
① 先看下没用事件委托之前,是如何实现的:
<ul id="ul1">
<li>11</li>
<li>22</li>
<li>33</li>
<li>44</li>
</ul>
实现功能是点击 li,弹出123:
window.onload = function () {
var oUl = document.getElementById('ul1')
let oLi = oUl.getElementsByTagName('li')
for (var i = 0; i < oLi.length; i++) {
console.log(oLi[i])
oLi[i].onclick = function () {
alert(123)
}
}
}
上面的代码,有很多次的 DOM 操作。
首先要找到 ul,然后遍历 li。然后点击 li 的时候,又要找一次目标的 li 的位置,才能执行最后的操作,每次点击都要找一次 li。很麻烦~
② 用事件委托的方式:
window.onload = function() {
var oUl = document.getElementById('ul1')
oUl.onclick = function() {
alert('利用事件冒泡的原理,将事件委托给父元素')
}
}
这里用父级 ul 做事件处理。当 li 被点击时,由于冒泡原理,事件就会冒泡到 ul 上。
因为 ul上有点击事件,所以事件就会触发。这里当点击ul的时候,也是会触发的。那么问题来了,如果我想在只有点击 li 的时候才会触发事件,怎么处理呢?
5、事件源
Event 对象提供了一个属性叫 target,可以返回事件的目标节点,我们称为事件源。
也就是说 target 就可以表示为当前的事件操作的DOM,但是不是真正操作DOM。
这个是有兼容性的:
标准浏览器用 ev.target,IE浏览器用 event.srcElement
此时只是获取了当前节点的位置,并不知道是什么节点名称。这里我们用 nodeName 来获取具体是什么标签名(这个返回的是一个大写的,我们需要转成小写再做比较):
// 这样改下就只有点击li会触发事件了,且每次只执行一次DOM操作
// 如果li数量很多的话,将大大减少DOM的操作,优化的性能可想而知~~
window.onload = function () {
var oUl = document.getElementById('ul1')
oUl.onclick = function (e) {
// 获取事件的目标节点 -- 事件源
console.log(e)
var ev = e || window.event;
// 标准浏览器 e.target IE浏览器 e.srcElement
var target = ev.target || ev.srcElement;
if (target.nodeName.toLowerCase() == 'li') {
alert('获取事件的目标节点')
}
}
}
6、 实现不同的点击效果
上面的例子是说 li 操作的是同样的效果,要是每个 li 被点击的效果都不一样,那么用事件委托还有用吗?
<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>
① 普通方法实现: 4个按钮,点击每一个做不同的操作,那么至少需要4次 DOM 操作
window.onload = function() {
var add = document.getElementById('add')
var remove = document.getElementById('remove')
var move = document.getElementById('move')
var select = document.getElementById('select')
add.onclick = function() {
alert('添加')
}
remove.onclick = function() {
alert('删除')
}
move.onclick = function() {
alert('移动')
}
select.onclick = function() {
alert('选择')
}
}
② 用事件委托方式:可以只用一次DOM操作就能完成所有的效果,比上面的性能要好一些。
window.onload = function() {
var oBox = document.getElementById('box')
oBox.onclick = function(e) {
console.log(e)
var ev = e || window.event
var target = ev.target || ev.srcElement
console.log(target)
switch (target.id) {
case 'add':
alert('add')
break
case 'remove':
alert('remove')
break
case 'move':
alert('move')
break
case 'select':
alert('select')
break
}
}
}
7、 新增的节点如何实现事件委托?
前面讲的都是document加载完成的现有DOM节点下的操作。如果是新增的节点,新增的节点会有事件吗?如何实现?
现在是移入li,li变红,移出li,li变白,这么一个效果。然后点击按钮,可以向ul中添加一个li子节点
① 一般的方法:
<ul id="ul1">
<li>11</li>
<li>22</li>
<li>33</li>
<li>44</li>
</ul>
<div id="box">
<input type="button" id="add" value="添加" />
</div>
新增的li是没有事件的,说明添加子节点的时候,事件没有一起添加进去,这不是我们想要的结果,那怎么做呢?一般的解决方案会是这样,将for循环用一个函数包起来,命名为mHover
window.onload = function() {
var oUl = document.getElementById('ul1')
var oLi = oUl.getElementsByTagName('li')
var add = document.getElementById('add')
var num = oLi.length
// li 移入底色变红 移出变白
function mHover() {
for (var i = 0; i < oLi.length; i++) {
oLi[i].onmouseover = function() {
this.style.background = 'red'
}
oLi[i].onmouseout = function() {
this.style.background = '#fff'
}
}
}
mHover()
// 添加新节点
add.onclick = function() {
num++
var newLi = document.createElement('li')
newLi.innerHTML = 11 * num
ul1.appendChild(newLi)
mHover()
}
}
虽然功能实现了,但实际上无疑是又增加了一个DOM操作。在优化性能方面是不可取的~
② 事件委托的方式:
window.onload = function() {
var ulEle = document.getElementById('ul1')
var addEle = document.getElementById('add')
var liLength = ulEle.getElementsByTagName('li').length
ulEle.onmouseover = function(e) {
var ev = e || window.event
var target = e.target || e.srcElement
if (target.nodeName.toLowerCase() === 'li') {
target.style.background = 'red'
}
}
ulEle.onmouseout = function(e) {
var ev = e || window.event
var target = e.target || e.srcElement
if (target.nodeName.toLowerCase() === 'li') {
target.style.background = '#fff'
}
}
addEle.onclick = function() {
liLength++
var newLiEle = document.createElement('li')
newLiEle.innerHTML = liLength * 11
ulEle.appendChild(newLiEle)
}
}
由上面的代码可以看出,新添加的子元素是带有事件效果的。
当用事件委托的时候,根本就不需要去遍历元素的子节点,只需要给父级元素添加事件就OK了,其他的都是在js里面的执行。
这样可以大大的减少DOM操作,这才是事件委托的精髓所在。
8、什么样的事件可以用事件委托,什么样的事件不可以用?
① 适合用事件委托的事件:
click,mousedown,mouseup,keydown,keyup,keypress。
值得注意的是,mouseover 和 mouseout 虽然也有事件冒泡,但是处理它们的时候需要特别的注意,因为需要经常计算它们的位置,处理起来不太容易。
② 不适合用事件委托的事件:
不适合的就有很多了。举个例子:
mousemove,每次都要计算它的位置,非常不好把控;
还有focus,blur之类的,本身就没用冒泡的特性,自然就不能用事件委托了。
9、事件委托的优缺点
① 优点:
减少事件注册,节省内存;
例如上面代码,只指定父元素的处理程序,即可管理所有所有子元素的“click”事件;
简化了DOM节点更新时,相应事件的更新;
② 缺点:
利用事件冒泡的原理,不支持不冒泡的事件;
层级过多,冒泡过程中,可能会被某层阻止掉;
建议就近委托;
理论上委托会导致浏览器频繁调用处理函数,虽然很可能不需要处理。所以建议就近委托,比如在ol上代理li,而不是在document上代理li。
把所有事件都用代理就可能会出现事件误判。
比如,在document中代理了所有button的click事件,另外的人在引用改js时,可能不知道,造成单击button触发了两个click事件。