共计 7158 个字符,预计需要花费 18 分钟才能阅读完成。
什么是事件
javascript 与 HTML 之间交互就是通过事件实现的,事件就是文档或浏览器窗口中发生的一些特定的交互瞬间。事件在浏览器中是以对象的形式存在的,即 event,触发一个事件,就会产生一个事件对象 event,该对象包含着所有与事件有关的信息,包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。
DOM 元素添加事件
1.DOM 元素事件属性
<button onclick=”clickFun()”>addEvent</button>
2.javascript 脚本添加事件
a. element.onclick=function(e){} b. element.addEventListener(event,function(e),useCapture)
适用范围:现代浏览器(IE9+, Firefox , chorme, safari, opera)参数介绍:event 必须,字符串,事件名称。(注:不要是 on+’someEvent’, just ‘someEvent’ is ok)。function 必须,指定事件触发时执行的函数。useCapture 可选,布尔值,指定事件是否在捕获或冒泡阶段执行,默认 false(冒泡)
c. element.attachEvent(event,function(e))
适用范围:IE 6、7、8 参数介绍:event 必须,字符串,事件名称。(注:含“on”,比如“onclick”、“onmouseover”、“onkeydown”等)。function 必须,指定事件触发时执行的函数。
栗子
<button id=”myBtn”> 点击这里 </button>
<script>
document.getElementById(“myBtn”).onclick=function(){clickFun()};
document.getElementById(“myBtn”).addEventListener(“click”,function(event){
clickFun();
},false); // 采用事件冒泡方式给 dom 元素注册 click 事件
document.getElementById(“myBtn”).addEventListener(“click”,function(event){
clickFun();
},true); // 采用事件捕获方式给 dom 元素注册 click 事件
document.getElementById(“myBtn”).attachEvent(“click”,function(event){
clickFun();
});
</script>
扩展:原生 Javascript 写法,跨浏览器添加事件
var EventUtil = {
// 添加事件监听
add: function(element, type, callback){
if(element.addEventListener){
element.addEventListener(type, callback, false);
} else if(element.attachEvent){
element.attachEvent(‘on’ + type, callback);
} else {
element[‘on’ + type] = callback;
}
}
// 移除事件监听
remove: function(element, type, callback){
if(element.removeEventListener){
element.removeEventListener(type, callback, false);
} else if(element.detachEvent){
element.detachEvent(‘on’ + type, callback);
} else {
element[‘on’ + type] = null;
}
}
}
<button id=”myBtn”> 点击这里 </button>
<script>
var myBtn = document.getElementById(“myBtn”);
EventUtil.add(myBtn, ‘click’, function(){
console.log(‘ 被点击了 ’);
});
事件捕获、事件冒泡
事件捕获
事件捕获(event capturing):当鼠标点击或者触发 dom 事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件。
事件冒泡
事件冒泡(dubbed bubbling):与事件捕获恰恰相反,事件冒泡顺序是由内到外进行事件传播,直到根节点。
注:无论是事件捕获还是事件冒泡,它们都有一个共同的行为,就是事件传播
阻止事件捕获 / 事件冒泡
event.stopPropagation()
event.stopImmediatePropagation()
event.stopPropagation() 会阻止事件继续分发到其他 document 节点, 但是当前节点绑定的多个事件会继续按注册的顺序执行
event.stopImmediatePropagation() 不仅阻止事件继续分发到其他 document, 还会将事件分发就地停止, 在当前事件之后注册的其他事件, 都不会执行
举个栗子:此栗子是阻止事件捕获,阻止事件冒泡原理一样
<div id=”div1″>
<button id=”button1″> 测试 </button>
</div>
<script type=”application/javascript”>
var div1 = document.getElementById(“div1”);
var btn = document.getElementById(“button1”);
div1.addEventListener(“click”, function () {
alert(“div1 第一次执行 ”);
event.stopPropagation();
}, true);
div1.addEventListener(“click”, function () {
alert(“div1 第二次执行 ”);
}, true);
btn.addEventListener(“click”, function (event) {
alert(“button 执行 ”);
}, true);
</script>
// 执行结果 alert(“div1 第一次执行 ”); alert(“div1 第二次执行 ”);
<div id=”div1″>
<button id=”button1″> 测试 </button>
</div>
<script type=”application/javascript”>
var div1 = document.getElementById(“div1”);
var btn = document.getElementById(“button1”);
div1.addEventListener(“click”, function () {
alert(“div1 第一次执行 ”);
event.stopImmediatePropagation();
}, true);
div1.addEventListener(“click”, function () {
alert(“div1 第二次执行 ”);
}, true);
btn.addEventListener(“click”, function (event) {
alert(“button 执行 ”);
}, true);
</script>
// 执行结果 alert(“div1 第一次执行 ”);
事件代理
什么是事件代理
事件代理也可以称之为事件委托,事件代理就是在祖先级 DOM 元素绑定一个事件,当触发子孙级 DOM 元素的事件时,利用事件冒泡的原理来触发绑定在祖先级 DOM 的事件。
那这是什么意思呢?网上的各位大牛们讲事件代理基本上都用了同一个例子,就是取快递来解释这个现象,借花献佛,大家认真领会一下事件代理到底是一个什么原理:有三个同事预计会在周一收到快递。为签收快递,有两种办法:一是三个人在公司门口等快递;二是委托给前台 MM 代为签收。现实当中,我们大都采用委托的方案。前台 MM 收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优势,那就是即使公司里来了新员工,前台 MM 也会在收到寄给新员工的快递后核实并代为签收。
这里其实有 2 层意思的:第一,现在委托前台的同事是可以代为签收的,即程序中的现有的 dom 节点是有事件的;第二,新员工也是可以被前台 MM 代为签收的,即程序中新添加的 dom 节点也是有事件的。
为什么要用事件代理
一般来说,dom 需要有事件处理程序,我们都会直接给它设事件处理程序就好了,那如果是很多的 dom 需要添加事件处理呢?比如我们有 100 个 li,每个 li 都有相同的 click 点击事件,可能我们会用 for 循环的方法,来遍历所有的 li,然后给它们添加事件,那这么做会存在什么影响呢?
在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能,因为需要不断的与 dom 节点进行交互,访问 dom 的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少 DOM 操作的原因;如果要用事件代理,就会将所有的操作放到 js 程序里面,与 dom 的操作就只需要交互一次,这样就能大大的减少与 dom 的交互次数,提高性能;
每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就越大,自然性能就越差了(内存不够用,是硬伤),比如上面的 100 个 li,就要占用 100 个内存空间,如果是 1000 个,10000 个呢,那只能呵呵了,如果用事件代理,那么我们就可以只对它的父级(如果只有一个父级)这一个对象进行操作,这样我们就需要一个内存空间就够了,是不是省了很多,自然性能就会更好。
事件代理的实现
在介绍事件代理的方法之前,先来看一段一般方法的栗子:
<ul id=”ulDelegate”>
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
<script>
window.onload = function(){
var ulDelegate= document.getElementById(“ulDelegate”);
var liDelegate= ulDelegate.getElementsByTagName(‘li’);
for(var i=0;i<liDelegate.length;i++){
liDelegate[i].onclick = function(){
alert(‘noDelegate’);
}
}
}
</script>
上面的代码很简单,我们看看有多少次的 dom 操作,首先要找到 ul,然后遍历 li,然后点击 li 的时候,又要找一次目标的 li 的位置,才能执行最后的操作,每次点击都要找一次 li。
用事件代理的方式来实现,上栗子:
window.onload = function(){
var ulDelegate= document.getElementById(“ulDelegate”);
ulDelegate.onclick = function(){
alert(‘Delegate’);
}
}
这里用父级 ul 做事件处理,当 li 被点击时,由于冒泡原理,事件就会冒泡到 ul 上,因为 ul 上有点击事件,所以事件就会触发,当然,这里当点击 ul 的时候,也是会触发的。
那么问题就来了,如果我想让事件代理的效果跟直接给节点的事件效果一样怎么办,比如说只有点击 li 才会触发,这时,Event 对象就派上用场了,Event 提供了一个属性叫 target,可以返回事件的目标节点,我们称为事件源,也就是说,target 就可以表示为当前的事件操作的 dom,但是不是真正操作 dom,这个是有兼容性的,标准浏览器用 ev.target,IE 浏览器用 event.srcElement,此时只是获取了当前节点的位置,并不知道是什么节点名称,这里我们用 nodeName 来获取具体是什么标签名,这个返回的是一个大写的,我们可以转成小写再做比较,或者不转。
栗子
window.onload = function(){
var ulDelegate= document.getElementById(“ulDelegate”);
ulDelegate.onclick = function(ev){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLowerCase() == ‘li’){
alert(‘Delegate’);
alert(target.innerHTML);
}
}
}
这样就只有点击 li 会触发事件了,且每次只执行一次 dom 操作,如果 li 数量很多的话,将大大减少 dom 的操作,优化的性能可想而知!
上面的栗子是说 li 操作的是同样的效果,如果每个 li 被点击的效果都不一样,上栗子:
<ul id=”ulDelegate”>
<li id=’add’> 添加 </li>
<li id=’delete’> 删除 </li>
<li id=’edit’> 修改 </li>
</ul>
<script>
window.onload = function(){
var ulDelegate= document.getElementById(“ulDelegate”);
ulDelegate.onclick = function (ev) {
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLocaleLowerCase() == ‘li’){
switch(target.id){
case ‘add’ :
alert(‘ 添加 ’);
break;
case ‘delete’ :
alert(‘ 删除 ’);
break;
case ‘edit’ :
alert(‘ 移动 ’);
break;
}
}
}
}
</script>
再来最后一个事件代理的栗子,当我们新增一个 li 元素的时候,同样先看一下一般写法:
<ul id=”ulDelegate”>
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
<button id=”addLi”> 新增 li</button>
<script>
var ulDelegate= document.getElementById(“ulDelegate”);
var liDelegate= ulDelegate.getElementsByTagName(‘li’);
var addLi = document.getElementById(“addLi”);
function addClick() {
for(var i=0;i<liDelegate.length;i++){
liDelegate[i].onclick = function(){
alert(‘noDelegate’);
}
}
}
addClick();
addLi.onclick = function(){
var addLiElement = document.createElement(‘li’);
addLiElement .innerHTML = 10000*Math.random();
ulDelegate.appendChild(addLiElement);
addClick();
}
</script>
再来看一下事件代理的写法:
window.onload = function(){
var ulDelegate = document.getElementById(“ulDelegate”);
var addLi = document.getElementById(“addLi”);
ulDelegate.onclick = function(ev){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLowerCase() == ‘li’){
alert(‘Delegate’);
alert(target.innerHTML);
}
};
addLi.onclick = function(){
var addLiElement = document.createElement(‘li’);
addLiElement .innerHTML = 10000*Math.random();
ulDelegate.appendChild(addLiElement);
}
}
看,上面是用事件单例的方式,新添加的子元素是带有事件效果的,我们不需要去遍历元素的子节点,只需要给父级元素添加事件就好了,其他的都是在 js 里面的执行,这样可以大大的减少 dom 操作,这才是事件代理的精髓所在。
总结
事件是将 javascript 与网页联系在一起的主要方式,在使用事件时,需要考虑如下一些内存与性能方面的问题。
有必要限制夜歌页面中事件处理程序的数量,数量太多会导致占用大量内存,而且也会让用户页面反应不够灵敏
建立在事件冒泡机制之上的时间委托技术,可以有效的减少时间处理程序的数量
建议在浏览器卸载页面之前移除页面中的所有事件处理程序
事件是 javascript 中最重要的主题之一,深入理解事件的工作机制以及它们对性能的影响是至关重要的
(总结摘自 javascript 高级程序设计第三版)