事件委托(事件代理)
出现事件委托的背景
事件委托有哪些好处,才会被现在人们大量的使用呢?
那么就得先说说事件的一些性能和使用的问题:
绑定事件越多,浏览器内存占用越大,严重影响性能。
ajax的出现,局部刷新的盛行,导致每次加载完,都要重新绑定事件
部分浏览器移除元素时,绑定的事件并没有被及时移除,导致的内存泄漏,严重影响性能
大部分ajax局部刷新的,只是显示的数据,而操作却是大部分相同的,重复绑定,会导致代码的耦合性过大,严重影响后期的维护。
这些个限制,都是直接给元素事件绑定带来的问题,所以经过了一些前辈的总结试验,也就有了事件委托这个解决方案。
浏览器的事件流
浏览器的事件流分为1.捕获阶段,eventPhase是 1; 2.目标阶段,eventPhase是2; 3.冒泡阶段,eventPhase是 3;
捕获阶段是从父元素到目标元素
冒泡阶段是目标元素到父元素
事件委托定义
事件委托通俗地来讲,就是把一个元素响应事件(click、keydown......)的函数委托到另一个元素;
就是在祖先级DOM元素绑定一个事件,当触发子孙级DOM元素的事件时,利用事件流的原理来触发绑定在祖先级DOM的事件。
当要对一系列的元素都添加响应事件时,可以只给父元素添加响应事件,然后利用事件冒泡对事件作出响应. 最普遍做法点击li打印事件
window.onload = function(){
var oUl = document.getElementById("ul");
var aLi = oUl.getElementsByTagName('li');
for(var i=0;i<aLi.length;i++){
aLi[i].onclick = function(){
alert(123);
}
}
}
2
3
4
5
6
7
8
9
用事件委托这么做
window.onload = function(){
var oUl = document.getElementById("ul1");
oUl.onclick = function(ev){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLowerCase() == 'li'){
alert(123);
alert(target.innerHTML);
}
}
}
2
3
4
5
6
7
8
9
10
11
优点
- 页面的上的事件会影响事件的性能,事件过多,会导致网页的性能下降,采用事件委托,提高性能,可以大量节省内存占用,减少事件注册。比如ul上代理所有li的click事件就很不错。
- 可以实现当新增子对象时,无需再对其进行事件绑定,对于动态内容部分尤为合适
- 不用担心某个注册了事件的DOM元素被移除后,可能无法回收其事件处理程序,我们只要把事件处理程序委托给更高层级的元素,就可以避免此问题
局限
- focus、blur之类的事件本身没有事件冒泡机制,所以无法委托;
- mousemove、mouseout这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合事件委托的
- 层级过多,冒泡过程中,可能会被某层阻止掉(建议就近委托)
- 事件代理的常用应用应该仅限于上述需求,如果把所有事件都用事件代理,可能会出现事件误判。即本不该被触发的事件被绑定上了事件。
额外拓展
1. 事件流描述的是从页面中接受事件的顺序。
2. 事件处理程序
HTML事件处理程序
DOM0级事件处理程序
DOM2级事件处理程序
- DOM2级事件定义了两个方法:用于处理指定和删除事件处理程序的操作:addEventListener()和removeEventListener()。
它们都接收三个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。
IE事件处理程序
- attachEvent()添加事件
- detachEvent()删除事件
这两个方法接收相同的两个参数:事件处理程序名称与事件处理函数
3. 事件对象
DOM中的事件对象
- type 获取事件类型
- target 事件目标
- stopPropagation() 阻止事件冒泡
- preventDefault() 阻止事件的默认行为
IE中的事件对象
- type 获取事件类型
- srcElement 事件目标
- cancelBubble=true 阻止事件冒泡
- returnValue=false 阻止事件的默认行为
4. 事件处理程序详解
- HTML事件处理程序
它是写在HTML里的,是全局作用域。
<button onclick="alert('hello')"></button>
当我们需要使用一个复杂的函数时,将js代码写在这里,显然很不合适,所以有了下面这种写法:
<!-- 点击事件触发doSomething()函数,这个函数写在单独的js或<script>之中 -->
<button onclick="doSomething()"></button>
2
这样会出现一个时差问题,当用户在HTML元素出现一开始就进行点击,有可能js还没加载好,这时候就会报错。但我们可以将函数封装在try-catch来处理:
<button onclick="try{doSomething();}catch(err){}"></button>
同时,一个函数的改变,同时可能会涉及html和js的修改,这样是很不方便的,综上,才有了DOM0 级事件处理程序。
- DOM0 级事件处理程序
<button id="btn">点击</button>
<script>
var btn = document.getElementById('btn');
btn.onclick = function() {
alert('hello');
}
</script>
2
3
4
5
6
7
8
可以看到button.onlick这种形式,这里事件处理程序作为btn对象的方法,是局部作用域。
所以我们可以用
btn.onclick = null; // 删除指定的事件处理程序
如果我们尝试添加两个事件:
<button id="btn">点击</button>
<script>
var btn = document.getElementById('btn');
btn.onclick = function() {
alert('hello');
}
btn.onclick = function() {
alert('hello again');
}
</script>
2
3
4
5
6
7
8
9
10
11
12
结果输出hello again,很明显第一个事件函数被第二个事件函数给覆盖了。所以,DOM0 级事件处理程序不能添加多个,也不能控制事件流到底是捕获还是冒泡。
DOM2 级事件处理程序(不支持IE)
进一步规范之后,有了DOM2 级事件处理程序,其中定义了两个方法:
- addEventListener:添加事件侦听器
- removeEventListener:删除事件侦听器
这两个方法都有三个参数:
- 第一个参数:要处理的事件名(不带on的前缀才是事件名)
- 第二个参数:作为事件处理程序的函数
- 第三个参数:是一个boolean值,默认false表示使用冒泡机制,true表示捕获机制
<button id="btn">点击</button> <script> var btn = document.getElementById('btn'); btn.addEventListener('click', 'hello', false); btn.addEventListener('click', 'helloAgain', false); function hello() { alert('hello'); } function helloAgain() { alert('hello again'); } </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16这时候,这两个事件处理程序都能够被触发,说明可以绑定多个事件处理程序,但是注意,如果定义了一模一样的监听方法,是会发生覆盖的,即同样的事件和事件流机制下相同方法只会触发一次。比如:
<button id="btn">点击</button> <script> var btn = document.getElementById('btn'); btn.addEventListener('click', 'hello', false); btn.addEventListener('click', 'hello', false); function hello() { alert('hello'); } </script>
1
2
3
4
5
6
7
8
9
10
11
12removeEventListener()的用法几乎和添加时的用法一模一样:
<button id="btn">点击</button> <script> var btn = document.getElementById('btn'); btn.addEventListener('click', 'hello', false); btn.removeEventListener('click', 'hello', false); function hello() { alert('hello'); } </script>
1
2
3
4
5
6
7
8
9
10
11
12这样的话,事件处理程序只会执行一次。
但是要注意,如果同一个监听事件分别为“事件捕获”和“事件冒泡”注册了一次,一共两次,这两次事件需要分别移除。两者不会互相干扰。
这时候的this指向该元素的引用。这里事件触发的顺序是添加的顺序。
IE事件处理程序
对于IE来说,在IE9之前,你必须使用attachEvent而不是使用标准方法addEventListener。
IE事件处理程序中有类似DOM2 级事件处理程序的两个方法:
- attachEvent()
- detachEvent()
它们都接收两个参数:
- 事件处理程序名称:如onclick、onmouseover,注意:这里不是事件,而是事件处理程序的名称,所以有on
- 事件处理程序函数 之所以没有和DOM2 级事件处理程序中类似的第三个参数,是因为IE8及更早版本只支持冒泡事件流。
<button id="btn">点击</button> <script> var btn = document.getElementById('btn'); btn.attachEvent('onclick', hello); btn.detachEvent('onclick', hello); function hello() { alert('hello'); } </script>
1
2
3
4
5
6
7
8
9
10
11
12注意:这里事件触发的顺序不是添加的顺序而是添加顺序的想法顺序。
使用attachEvent方法有个缺点,this的值会变成window对象的引用而不是触发事件的元素。
事件对象
事件对象是用来记录一些事件发生时的相关信息的对象。事件对象只有事件发生时才会产生,并且只能是事件处理程序内部访问,在所有事件处理函数运行结束后,事件对象就被销毁。
- 2级DOM中的Event对象
常用的属性和方法:
- type: 获取事件类型
- target:触发此事件的元素(事件的目标节点)
- preventDefault():取消事件的默认操作,比如链接的跳转或者表单的提交,主要是用来阻止标签的默认行为
- stopPropagation():冒泡机制下,阻止事件的进一步网上冒泡
- IE中的Event对象
常用的属性和方法:
- type:事件类型
- srcElement:事件目标
- 取消事件的默认操作:returnvalue = false
- 阻止事件冒泡:cancelBubble = false
兼容性 事件对象也存在一定的兼容性问题,在IE8及以前版本之中,通过设置属性注册事件处理程序时,调用的时候并未传递事件对象,需要通过全局对象window.event来获取。解决方法如下:
function getEvent(event) { event = event || window.event; }
1
2
3如果希望事件到某个节点为止,不再传播,可以使用事件对象的stopPropagation()方法。
// 事件传播到p元素后,就不再向下传播了 p.addEventListener('click', function (event) { event.stopPropagation(); }, true); // 事件冒泡到p元素后,就不再向上冒泡了 p.addEventListener('click', function (event) { event.stopPropagation(); }, false);
1
2
3
4
5
6
7
8
9上面代码中,stopPropagation方法分别在捕获阶段和冒泡阶段,阻止了事件的传播。
注意:stopPropagation方法只会阻止事件的传播,不会阻止该事件触发p节点的其他click事件的监听函数。也就是说,不是彻底取消click事件。
p.addEventListener('click', function (event) { event.stopPropagation(); console.log(1); }); p.addEventListener('click', function (event) { // 会触发 console.log(2); });
1
2
3
4
5
6
7
8
9上面代码中,p元素绑定了两个click事件的监听函数。stopPropagation方法只能阻止这个事件向其他元素传播。因此,第二个监听函数会触发,输出结果会先是1,再是2。
如果想要彻底阻止这个事件的传播,不再触发后面所有click的监听函数,可以使用stopImmediatePropagation方法。
p.addEventListener('click', function (event) { event.stopImmediatePropagation(); console.log(1); }); p.addEventListener('click', function (event) { // 不会被触发 console.log(2); });
1
2
3
4
5
6
7
8
9