JavaScript事件---事件绑定和深入
发文不易,转载传播,请亲注明链接出处,谢谢!
内容提纲:
1.传统事件绑定的问题
2.W3C事件处理函数
3.IE事件处理函数
4.事件对象的其他内容
事件绑定分为两种:一种是传统事件绑定(内联模型,脚本模型),一种是现代事件绑定(DOM2级模型)。现代事件绑定在传统绑定上提供了更强大更方便的功能。
一.传统事件绑定的问题
传统事件绑定有内联模型和脚本模型,内联模型我们不做讨论,基本很少去用。先来看一下脚本模型,脚本模型将一个函数赋值给一个事件处理函数。
1 var box = document.getElementById('box'); //获取元素 2 3 box.onclick = function () { //元素点击触发事件 4 5 alert('Lee'); 6 7 };
问题一:一个事件处理函数触发两次事件
window.onload = function () { //第一组程序项目或第一个JS文件
alert('Lee');
};
window.onload = function () { //第二组程序项目或第二个JS文件
alert('Mr.Lee');
};
当两组程序或两个JS文件同时执行的时候,后面一个会把前面一个完全覆盖掉。导致前面的window.onload完全失效了。
解决覆盖问题,我们可以这样去解决:
PS:如果一开始没有window.onload,旧版火狐显示undefined,新版显示object,谷歌和IE浏览器也是object;如果有window.onload,所有浏览器都会显示function。
1 window.onload = function () { //第一个要执行的事件,会被覆盖 2 3 alert('Lee'); 4 5 }; 6 7 if (typeof window.onload == 'function') { //判断之前是否有window.onload 8 9 var saved = null; //创建一个保存器 10 11 saved = window.onload; //把之前的window.onload保存起来 12 13 } 14 15 window.onload = function () { //最终一个要执行事件 16 17 if (saved) saved(); //执行之前一个事件 18 19 alert('Mr.Lee'); //执行本事件的代码 20 21 };
问题二:事件切换器
window.onload = function () {
var box = document.getElementById('box');
box.onclick =toBlue; //第一次执行toBlue()
};
function toRed() {
this.className = 'red';
this.onclick = toBlue; //第三次执行toBlue(),然后来回切换
}
PS:直接将函数绑定给事件处理函数,toBlue里面的this就是事件对象box,否则是window!(事件入门一篇提到过)
function toBlue() {
this.className = 'blue';
this.onclick = toRed; //第二次执行toRed()
}
这个切换器在扩展的时候,会出现一些问题:
1.如果增加一个执行函数,那么会被覆盖
box.onclick = toAlert; //被增加的函数
box.onclick = toBlue; //toAlert被toBlue覆盖了
2.如果解决覆盖问题,就必须包含到一起,然后同时执行,但又出新问题
box.onclick = function () { //包含进去,但可读性降低
//第一次不会被覆盖,但第二次又被覆盖(被toBlue里面的onclick覆盖)
toAlert();
toBlue.call(this); //还必须把this传递到切换器里
};
PS:通过匿名函数执行一个函数,此函数里面的this是window,所以必须在匿名函数中把this传递过去!
综上的三个问题:覆盖问题、可读性问题、this传递问题。我们来创建一个自定义的事件处理函数,来解决以上三个问题。
//添加事件函数
//obj相当于window
//type相当于onload
//fn相当于function () {}
//组合起来:window.onload = function(){}
1 function addEvent(obj, type, fn) { //取代传统事件处理函数 2 3 var saved = null; //保存每次触发的事件处理函数 4 5 if (typeof obj['on' + type] == 'function') { //判断是不是事件 6 7 saved = obj['on' + type]; //如果有,保存起来 8 9 } 10 11 obj['on' + type] = function () { //然后执行window.onload相当于window['onload']; 12 13 if (saved) saved(); //执行上一个 14 15 fn.call(this); //执行函数,把this传递过去 16 17 }; 18 19 } 20 23 addEvent(window, 'load', function () { //执行到了 24 25 alert('Lee'); 26 27 }); 28 29 addEvent(window, 'load', function () { //执行到了 30 31 alert('Mr.Lee'); 32 33 });
PS:以上编写的自定义事件处理函数,还有一个问题没有处理,就是两个相同函数名的函数误注册了两次或多次,那么应该把多余的屏蔽掉。那我们就需要把事件处理函数进行遍历,如果有同样名称的函数名就不添加即可。(这里就不做了)
addEvent(window, 'load', init); //注册第一次
addEvent(window, 'load', init); //注册第二次,应该忽略
function init() {
alert('Lee');
}
用自定义事件函数注册到切换器上查看效果:
1 addEvent(window, 'load', function () { 2 3 var box = document.getElementById('box'); 4 5 addEvent(box, 'click', function () { //增加一个执行函数,每次都执行,不会被覆盖 6 7 alert('Mr.Lee'); 8 9 }); 10 11 addEvent(box, 'click', toBlue); 12 13 }); 14 15 16 17 function toRed() { 18 19 this.className = 'red'; 20 21 addEvent(this, 'click', toBlue); 22 23 } 24 25 26 27 function toBlue() { 28 29 this.className = 'blue'; 30 31 addEvent(this, 'click', toRed); 32 33 }
PS:当你单击很多很多次切换后,浏览器直接卡死,或者弹出一个错误:too much recursion(太多的递归)。主要的原因是,每次切换事件的时候,都保存下来,没有把无用的移除,导致越积越多,最后卡死(解决方案就是用完的事件及时的移除掉)。
//删除事件函数
1 function removeEvent(obj, type) { 2 3 if (obj['on'] + type) obj['on' + type] = null; //删除事件处理函数 4 5 }
以上的删除事件处理函数只不过是一刀切的删除了,这样虽然解决了卡死和太多递归的问题。但其他的事件处理函数也一并被删除了,导致最后得不到自己想要的结果。如果想要只删除指定的函数中的事件处理函数,那就需要遍历,查找。(这里就不做了,提示:在上面的删除函数中加上第三个参数fn)
二.W3C事件处理函数
“DOM2级事件”定义了两个方法,用于添加事件和删除事件处理程序的操作:addEventListener()和removeEventListener()。所有DOM节点中都包含这两个方法,并且它们都接受3个参数;事件名、函数、冒泡或捕获的布尔值(true表示捕获,false表示冒泡)。
//问题1:覆盖问题(解决)
1 window.addEventListener('load', function () { 2 3 alert('Lee'); 4 5 }, false); 6 7 8 window.addEventListener('load', function () { 9 10 alert('Mr.Lee'); 11 12 }, false);
//问题2:屏蔽掉相同函数问题(解决)
1 window.addEventListener('load', init, false); //第一次执行了 2 3 window.addEventListener('load', init, false); //第二次被屏蔽了 4 5 function init() { 6 7 alert('Lee'); 8 9 }
//问题3:是否传递了this
1 window.addEventListener('load', function () { 2 3 var box = document.getElementById('box'); 4 5 box.addEventListener('click', function(){ 6 7 alert(this); //这儿的this是box 8 9 }, false); 10 11 },false);
//修改事件切换器
1 window.addEventListener('load', function () { 2 3 var box = document.getElementById('box'); 4 5 box.addEventListener('click', toBlue, false); 6 7 },false); 8 9 10 function toRed() { 11 12 this.className = 'red'; 13 14 this.removeEventListener('click', toRed, false); 15 16 this.addEventListener('click', toBlue, false); 17 18 } 19 20 21 function toBlue() { 22 23 this.className = 'blue'; 24 25 this.removeEventListener('click', toBlue, false); 26 27 this.addEventListener('click', toRed, false); 28 29 }
//问题4:添加一个额外的方法会被覆盖或者只能执行一次(解决)
1 window.addEventListener('load', function () { 2 3 var box = document.getElementById('box'); 4 5 box.addEventListener('click', function () { //不会被误删 6 7 alert('Mr.Lee'); 8 9 }, false); 10 11 box.addEventListener('click', toBlue, false); //引入切换也不会因太多递归卡死 12 13 }, false); 14 15 16 function toRed() { 17 18 this.className = 'red'; 19 20 this.removeEventListener('click', toRed, false); 21 22 this.addEventListener('click', toBlue, false); 23 24 } 25 26 27 function toBlue() { 28 29 this.className = 'blue'; 30 31 this.removeEventListener('click', toBlue, false); 32 33 this.addEventListener('click', toRed, false); 34 35 }
综上所述:W3C是比较完美的解决了这些问题,非常好用;但是IE8和之前的浏览器并不支持,而是采用了自己的事件,当然,IE9已经完全支持!
设置冒泡和捕获阶段
在事件入门一篇中介绍了事件冒泡,即从里到外触发。我们也可以通过event对象来阻止某一阶段的冒泡。
W3C现代事件绑定可以设置冒泡和捕获。把最后的布尔值设置成true,则为捕获;设置成false,则为冒泡。
//设置为true,捕获
document.addEventListener('click', function () {
alert('document');
}, true);
box.addEventListener('click', function () {
alert('div');
}, true);
三.IE事件处理函数
IE实现了与DOM中类似的两个方法:attachEvent()和detachEvent()。这两个方法接受相同的参数:事件名称和函数。
在使用这两组函数的时候,与W3C区别如下:
1.IE不支持捕获,只支持冒泡;
2.IE添加事件不能屏蔽重复的函数;
3.IE中的this指向的是window而不是DOM对象;
4.在传统事件上,IE是无法接受到event对象的,但使用了attchEvent()却可以,但有些区别。
//问题1:覆盖问题(解决,但有不同,输出顺序相反)
1 window.attachEvent('onload', function () { 2 3 alert('Lee'); 4 5 }); 6 7 window.attachEvent('onload', function () { 8 9 alert('Mr.Lee'); 10 11 }); 12 13 window.attachEvent('onload', function () { 14 15 alert('Miss.Lee'); 16 17 });
//问题2:相同函数屏蔽的问题(未解决)
1 window.attachEvent('onload', init); 2 3 window.attachEvent('onload', init); 4 5 6 function init() { 7 8 alert('Mr.Lee'); 9 10 }
//问题3:不可以传递this(未解决)
1 window.attachEvent('onload', function () { 2 3 var box = document.getElementById('box'); 4 5 box.attachEvent('onclick', function () { 6 7 //alert(this === box); //false 8 9 alert(this === window); //不能传递this 10 11 }); 12 13 }); 14 15 //可以通过call()传递过去 16 17 window.attachEvent('onload', function () { 18 19 var box = document.getElementById('box'); 20 21 box.attachEvent('onclick', function () { 22 23 toBlue.call(box); //把this直接call过去(但切换器我们不这样做!) 24 25 }); 26 27 }); 28 29
//问题4:添加一个额外的方法会被覆盖或者只能执行一次(解决)
window.attachEvent('onload', function () { var box = document.getElementById('box'); box.attachEvent('onclick', function () { alert('Lee'); }); box.attachEvent('onclick', function () { alert('Mr.Lee'); }); });
在传统绑定上,IE是无法像W3C那样通过传参接受event对象,但如果使用了attachEvent()却可以。
1 window.attachEvent('onload', function () { 2 3 var box = document.getElementById('box'); 4 5 //box.onclick = function (evt) { //传统方法IE无法通过参数获取evt 6 7 // alert(evt); 8 9 //} 10 11 box.attachEvent('onclick', function (evt) { //IE的现代事件绑定机制是可以的 12 13 //alert(evt); //object 14 15 //alert(evt.type); //click 16 17 //alert(evt.srcElement.tagName); //box, 这个可以有 18 19 alert(window.event.srcElement.tagName); //box, 这个更可以有 20 21 }); 22 23 });
//IE事件切换器
1 window.attachEvent('onload', function () { 2 3 var box = document.getElementById('box'); 4 5 box.attachEvent('onclick', toBlue); 6 7 }); 8 9 10 function toRed() { 11 12 var that = window.event.srcElement; 13 14 that.className = 'red'; 15 16 that.detachEvent('onclick', toRed); 17 18 that.attachEvent('onclick', toBlue); 19 20 } 21 22 23 function toBlue() { 24 25 var that = window.event.srcElement; 26 27 that.className = 'blue'; 28 29 that.detachEvent('onclick', toBlue); 30 31 that.attachEvent('onclick', toRed); 32 33 }
最后,为了让IE和W3C可以兼容这个事件切换器,我们写成如下方式:
1 function addEvent(obj, type, fn) { //添加事件兼容 2 3 if (obj.addEventListener) { 4 5 obj.addEventListener(type, fn); 6 7 } else if (obj.attachEvent) { 8 9 obj.attachEvent('on' + type, fn); 10 11 } 12 13 } 14 15 16 17 function removeEvent(obj, type, fn) { //移除事件兼容 18 19 if (obj.removeEventListener) { 20 21 obj.removeEventListener(type, fn); 22 23 } else if (obj.detachEvent) { 24 25 obj.detachEvent('on' + type, fn); 26 27 } 28 29 } 30 31 32 33 function getTarget(evt) { //得到事件目标 34 35 if (evt.target) { 36 37 return evt.target; 38 39 } else if (window.event.srcElement) { 40 41 return window.event.srcElement; 42 43 } 44 45 } 46 47 48 49 addEvent(window, 'load', function () { 50 51 var box = document.getElementById('box'); 52 53 addEvent(box, 'click', toBlue); 54 55 }); 56 57 58 59 function toRed(evt) { 60 61 var that = getTarget(evt); 62 63 that.className = 'red'; 64 65 removeEvent(that, 'click', toRed); 66 67 addEvent(that, 'click', toBlue); 68 69 } 70 71 72 73 function toBlue(evt) { 74 75 var that = getTarget(evt); 76 77 that.className = 'blue'; 78 79 removeEvent(that, 'click', toBlue); 80 81 addEvent(that, 'click', toRed); 82 83 } 84 85
PS:调用忽略,IE兼容的事件,如果要传递this,改成call即可(上面问题3部分说过了,但是一般不使用这种形式)。
PS:IE中的事件绑定函数attachEvent()和detachEvent()可能在实践中不去使用,有几个原因:
1.IE9就将全面支持W3C中的事件绑定函数;
2.IE的事件绑定函数无法传递this;
3.IE的事件绑定函数不支持捕获;
4.同一个函数注册绑定后,没有屏蔽掉;
5.有内存泄漏的问题。
至于怎么替代,这儿暂时不做探讨···
四.事件对象的其他内容
1.获取移入移出对象
在W3C提供了一个属性:relatedTarget;这个属性可以在mouseover和mouseout事件中获取从哪里移入和从哪里移出的DOM对象。
box.onmouseover = function (evt) { //鼠标移入box
alert(evt.relatedTarget); //获取移入box最近的那个元素对象
}
box.onmouseout = function (evt) { //鼠标移出box
alert(evt.relatedTarget); //获取移出box最近的那个元素对象
}
IE提供了两组分别用于移入移出的属性:fromElement和toElement,分别对应mouseover和mouseout。
box.onmouseover = function (evt) { //鼠标移入box
alert(window.event.fromElement.tagName); //获取移入box最近的那个元素对象
}
box.onmouseout = function (evt) { //鼠标移入box
alert(window.event.toElement.tagName); //获取移入box最近的那个元素对象
}
跨浏览器兼容:
1 function getTarget(evt) { 2 3 var e = evt || window.event; //得到事件对象 4 5 if (e.srcElement) { //如果支持srcElement,表示IE 6 7 if (e.type == 'mouseover') { //如果是mouseover 8 9 return e.fromElement; //就使用fromElement 10 11 } else if (e.type == 'mouseout') { //如果是mouseout 12 13 return e.toElement; //就使用toElement 14 15 } 16 17 } else if (e.relatedTarget) { //如果支持relatedTarget,表示W3C 18 19 return e.relatedTarget; 20 21 } 22 23 }
2.阻止默认行为
有时我们需要阻止事件的默认行为,比如:一个超链接的默认行为就点击然后跳转到指定的页面。那么阻止默认行为就可以屏蔽跳转的这种操作,而实现自定义操作。
取消事件默认行为还有一种不规范的做法,就是返回false。
link.onclick = function () {
alert('Lee');
return false; //直接给个false,就不会跳转了。
};
PS:虽然return false;可以实现这个功能,但有漏洞:
第一:必须写到最后,这样导致中间的代码执行后,有可能执行不到return false;
第二:return false写到最前那么之后的自定义操作就失效了。
所以,最好的方法应该是在最前面就阻止默认行为,并且后面还能执行代码。
1 link.onclick = function (evt) { 2 3 evt.preventDefault(); //W3C阻止默认行为,放哪里都可以 4 5 alert('Mr.Lee'); 6 7 }; 8 9 10 11 link.onclick = function (evt) { 12 13 window.event.returnValue = false; //IE阻止默认行为 14 15 alert('Mr.Lee'); 16 17 }; 18 19
跨浏览器兼容:
1 function preDef(evt) { 2 3 var e = evt || window.event; 4 5 if (e.preventDefault) { 6 7 e.preventDefault(); 8 9 } else { 10 11 e.returnValue = false; 12 13 } 14 15 }
3.上下文菜单事件
上下文菜单事件:contextmenu,当我们右击网页的时候,会自动出现windows自带的菜单。那么我们可以使用contextmenu事件来修改我们指定的菜单,前提是把右击的默认行为取消掉。
小示例:
html代码部分:
1 <body> 2 3 <textarea id="text" style="width:200px;height:100px;"></textarea> 4 5 <ul id="menu"> 6 7 <li>菜单1</li> 8 9 <li>菜单2</li> 10 11 <li>菜单3</li> 12 13 </ul> 14 15 </body>
css代码部分:
1 #menu { 2 3 width:50px; 4 5 background:grey; 6 7 position:absolute; 8 9 display:none; 10 11 }
JS代码部分:
addEvent(window, 'load', function () { var text = document.getElementById('text'); addEvent(text, 'contextmenu', function (evt) { preDef(evt); var menu = document.getElementById('menu'); var e = evt || window.event; menu.style.left = e.clientX + 'px'; menu.style.top = e.clientY + 'px'; menu.style.display = 'block'; addEvent(document, 'click', function () { menu.style.display = 'none'; }); }); });
PS:contextmenu事件很常用,而且此事件各浏览器兼容性较为稳定。
4.卸载前事件
卸载前事件:beforeunload,这个事件可以帮助在离开本页的时候给出相应的提示,“离开”或者“返回”操作。
addEvent(window, 'beforeunload', function (evt) {
preDef(evt); //必须要有,默认形式
});
5.鼠标滚轮事件
鼠标滚轮(mousewheel)和DOMMouseScroll,用于获取鼠标上下滚轮的距离。
1 addEvent(document, 'mousewheel', function (evt) { //非火狐 2 3 alert(getWD(evt)); 4 5 }); 6 7 addEvent(document, 'DOMMouseScroll', function (evt) { //火狐 8 9 alert(getWD(evt)); 10 11 }); 12 13 14 15 function getWD(evt) { 16 17 var e = evt || window.event; 18 19 if (e.wheelDelta) { 20 21 return e.wheelDelta; 22 23 } else if (e.detail) { 24 25 return -evt.detail * 30; //保持计算的统一 26 27 } 28 29 }
PS:通过浏览器检测可以确定火狐只执行DOMMouseScroll。
6.其他
DOMContentLoaded事件和readystatechange事件(很重要),有关DOM加载方面的事件,关于这两个事件的内容非常多,这儿就暂时不聊了!
for my lover and
thank you Mr.Lee!