JS 事件绑定(超大章)
w3c提供了addEventListener和removeEventListener两个方法,而IE提供的是attachEvent和detachEvent;但是ie的方法有很多问题,那么我们要解决以下这些问题:
- 支持同一个元素同一个事件绑定多个监听函数,如window.onload可以有多个,并且都是可以正常执行的。
- 同一个元素的相同事件注册同一个函数,只会运行一次。
- 函数体内的this应当指向调用该事件的对象本身。
- 监听函数的执行顺序应当按照绑定的顺序执行。
- 在函数体内event对象不使用event||window.event来标准化event对象。
跨浏览器添加事件:
function addEvent(obj, type, fn) = {
if (typeof addEventListener != 'undefined') {
obj.addEventListener(type, fn, false);
} else if (typeof attachEvent != 'undefined') {
obj.attachEvent('on' + type, fn);
}
}
跨浏览删除事件:
function removeEvent(obj, type, fn) = {
if (typeof removeEventEventListener != 'undefined') {
obj.removeEventListener(type, fn, false);
} else if (typeof detachEvent != 'undefined') {
obj.detachEvent ('on' + type, fn);
}
}
这里可以解决同一个对象绑定同一个事件并且可以正常运行,虽然w3c的方法可以让重复的事件函数只运行一次,但是ie的会全部运行,并且顺序还是错乱的。
然后ie的attachEvent本身也有event对象,所以这里可以不用event||window.event来标准化event对象。
解决this指向问题:
ie里面,this指向的是window对象!我们可以在attachEvent里面做一个匿名函数,然后在匿名函数里调用fn,调用的时候使用call方法改变this的指向。
function addEvent(obj,type,fn){
if(typeof addEventListener != 'undefined') {
obj.addEventListener(type,fn,false);
}else if(typeof attachEvent != 'undefined'){
obj.attachEvent('on'+type,function(){
fn.call(obj);
});
}
}
//调用
addEvent(document,'click',function(){
alert(this.nodeName);
})
在匿名函数里面使用call方法改变指向,但是这样event对象就无法正常传输了,这里我们可以使用call的第二个参数来传递这个event对象。
function addEvent(obj,type,fn){
if(typeof addEventListener != 'undefined') {
obj.addEventListener(type,fn,false);
}else if(typeof attachEvent != 'undefined'){
obj.attachEvent('on'+type,function(e){
fn.call(obj,e); //或者fn.call(obj,window.event);这样匿名函数就不要e作为参数了
});
}
}
//调用
addEvent(document,'click',function(e){
alert(e.clientX);
})
这样可以解决event对象的问题,但是使用匿名函数有一个非常严重的问题,就是无法删除这个事件了。
removeEvent(document,'click',function(e){
alert(e.clientX);
});
这样写是无法删除这个事件的,因为attachEvent添加的是一个匿名函数,你这里删除的是另一个函数,他没有对应,所以无法删除。
那么我们总结一下有以下几个问题无法解决:
- 无法删除事件
- 无法顺序执行
- ie现代事件的内存泄漏问题
- 相同事件和函数无法只执行一次
由于ie现代事件绑定有上面三个问题无法解决,我们要改用传统事件绑定的方式来处理。
如果使用了传统事件,那么顺序问题会被解决,然后就是重复问题会被解决,应为传统事件如果相同,最后一个会覆盖前一个,内存泄漏问题也没有了。
使用传统方式兼容:
function addEvent(obj,type,fn){
if(typeof addEventListener != 'undefined') {
obj.addEventListener(type,fn,false);
}else{
obj['on' + type] = function() {
fn.call(this,window.event);
};
}
}
//调用
addEvent(document,'click',function(e){
alert(this.nodeName);
alert(e.clientX);
})
匿名函数里面使用call改变this的指向,然后将event对象作为第二个参数传入,调用函数是e来接收,于是就可以了。
但是这样的话,同一个事件无法绑定多个函数。
为此我们可以使用数组来保存,比如click事件一个数组,dblclick一个数组,但是事件有很多,那么数组就要用一个对象来保存,对象里面通过键对值的方式保存事件数组。
var events = {
click : [fn1, fn2, fn3],
dblclick : [cn1, cn2, cn3]
}
这样写也会导致删除事件的时候无法删除,因为events对象你没有传入到removeEvent()函数中,这里,我们可以对obj这个对象做文章,我们可以给他添加一个自定义的属性对象:obj.events;我们用这个对象来保存事件数组,removeEvent里面也会传入obj,那也就间接的将events传入了嘛!
于是:
obj.events = {
click : [fn1, fn2, fn3],
dblclick : [cn1, cn2, cn3]
}
现在就要考虑将fn出入到数组中,有两种方法,一种是简单一点的,就是push()方法,还有一种就是模拟数组下标,模拟的话要在window下创建一个数组id,方便理解使用,名为addEvent.ID = 0;然后每次传入时obj.eventstype = fn; addEvent.ID++第一次表示为0,第二次时递增+1表示为1,依次。
于是:
function addEvent(obj, type, fn) {
if (typeof addEventListener != 'undefined') {
obj.addEventListener(type, fn, false);
} else {
if (!obj.events) obj.events = {};
if (!obj.events[type]) obj.events[type] = [];
obj.events[type].push(fn);
obj['on' + type] = function() {
for (var i in obj.events[type]) {
this.events[type][i].call(this, window.event);
}
}
}
}
//调用
addEvent(document, 'click', function(e) {
alert(this.nodeName);
alert(e.clientX);
})
将fn保存在数组中,然后在事件函数中调用,由于事件函数里面的函数this不是指向本身,所以使用call方法改变this指向,并且还可以传入window.event对象解决event标准化的问题。
删除事件函数:
function removeEvent(obj, type, fn) = {
if (typeof removeEventEventListener != 'undefined') {
obj.removeEventListener(type, fn, false);
} else {
for(var i in obj.events[type]) {
if(obj.events[type][i] == fn) {
delete obj.events[type][i];
}
}
}
}
删除的话想对简单很多,只需要删除对应的事件数组里保存的fn即可,使用if判断,如果相同,就使用delete删除。
精益求精,传统调用的时候,代码有些多,我们可以丢到外面封装一下:
function addEvent(obj, type, fn) {
if (typeof addEventListener != 'undefined') {
obj.addEventListener(type, fn, false);
} else {
if (!obj.events) obj.events = {};
if (!obj.events[type]) obj.events[type] = [];
obj.events[type].push(fn);
obj['on' + type] = exec;
}
}
}
//执行事件处理
function exec(event) {
var e = event || window.event;
var es = this.events[e.type];
for (var i in es) {
es[i].call(this,e);
}
}
//调用
addEvent(document, 'click', function(e) {
alert(this.nodeName);
alert(e.clientX);
})
这里因为丢到外面了, for (var i in obj.events[type])中的type无法接收到type的参数,所以要通过事件对象的type属性来获取,type可以获取到这是什么事件,如click事件,然后稍微美化一下,将 this.events[e.type]作为es来保存,其他都一样了,传入的windw.event的时候改用e,因为前面type属性也要用到window.event对象,所以这里使用e来保存window.envent。
下面使用模拟数组下标的方式:
function addEvent(obj,type,fn){
if(typeof addEventListener != 'undefined') {
obj.addEventListener(type,fn,false);
}else{
if(!obj.events) obj.events = {};
if(!obj.events[type]) obj.events[type] = [];
obj.events[type][addEvent.ID++] = fn;
obj['on' + type] = addEvent.exec;
}
}
//执行事件处理
addEvent.exec = function(event) {
var e = event || window.event;
var es = this.events[e.type];
for (var i in es) {
es[i].call(this,e);
}
}
//模拟数组下标
addEvent.ID = 0;
//调用
addEvent(document, 'click', function(e) {
alert(this.nodeName);
alert(e.clientX);
})
以上虽然很多问题都解决了,但是又产生了一个新的问题,因为我们是将fn保存在数组中 ,然后在事件函数里面依次运行的,这就导致重复的也会运行,所以我们还要在传入数组前做个判断。
if (!obj.events[type]) {
obj.events[type] = [];
} else {
if (addEvent.equa(obj.events[type], fn)) return;
}
//判断是否重复
addEvent.equa = function(es, fn) {
for (var i in es) {
if (es[i] == fn) return true;
}
return false;
}
在创建事件数组添加一个else,这样就可以说明这是第二次开始,那么从第二次开始就if判断,如果对应的事件数组中有对应的fn,那么就返回true,if执行return,这样下面的两句就不会运行,如果返回的false, 下面的两句运行。
阻止默认行为:
之前有做过一个兼容的函数,但是这里我们采用其他的方法来达到对应的效果。
我们采用模拟w3c的属性,为ie做兼容。
比如阻止a元素的超链接跳转行为:
<a id="a" href="https://www.mulingyuer.com">博客</a>
var a = document.getElementById('a');
addEvent(a, 'click', function(e) {
e.preventDefault();
})
这样写是w3c的写法,ie不支持,这里我们为ie做一个模拟方法,这里的阻止默认行为都是通过event对象来执行的,所以我们在addEvent()函数中就要模拟好对应的属性方法。
在上面的addEvent函数中,执行的addEvent.exec的时候,我们将window.event传给了调用时执行的fn函数,我们可以在这里添加好,再传给fn,这样执行fn的时候调用 e.preventDefault()就能生效。
//执行事件处理
addEvent.exec = function(event) {
var e = event || addEvent.fixEvent(window.event);
var es = this.events[e.type];
for (var i in es) {
es[i].call(this,e);
}
}
//模拟w3c阻止默认行为
addEvent.fixEvent = function(e) {
e.preventDefault = addEvent.fixEvent.preventDefault;
return e;
}
addEvent.fixEvent.preventDefault = function(){
this.returnValue = false;
}
为e添加了一个preventDefault 属性方法,然后再return出e,这样
addEvent.exec可以正常执行,并且e还多了一个preventDefault 的属性方法。
e.preventDefault 属性方法调用函数addEvent.fixEvent.preventDefault;因为preventDefault 也是一个事件,所以调用时不要加括号,然后调用时,一般是window.event.returnValue = false;的写法,但是这里e本身就是window.event,那么使用this就可以代表其本身,省去了传入e的步骤。
阻止冒泡:
使用传统的方式,我们并没有阻止冒泡,所以还要为ie添加一个阻止冒泡的属性方法,和阻止默认行为一样,也是模拟一个和w3c一样的方法。
addEvent.fixEvent = function(e) {
e.preventDefault = addEvent.fixEvent.preventDefault;
e.stopPropagation = addEvent.fixEvent.stopPropagation;
return e;
}
addEvent.fixEvent.preventDefault = function() {
this.returnValue = false;
}
addEvent.fixEvent.stopPropagation = function() {
this.cancelBubble = true;
}
以上完毕!
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据