正在加载中

最后更新于 2018年12月17日

w3c提供了addEventListener和removeEventListener两个方法,而IE提供的是attachEvent和detachEvent;但是ie的方法有很多问题,那么我们要解决以下这些问题:

  1. 支持同一个元素同一个事件绑定多个监听函数,如window.onload可以有多个,并且都是可以正常执行的。
  2. 同一个元素的相同事件注册同一个函数,只会运行一次。
  3. 函数体内的this应当指向调用该事件的对象本身。
  4. 监听函数的执行顺序应当按照绑定的顺序执行。
  5. 在函数体内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添加的是一个匿名函数,你这里删除的是另一个函数,他没有对应,所以无法删除。

那么我们总结一下有以下几个问题无法解决:

  1. 无法删除事件
  2. 无法顺序执行
  3. ie现代事件的内存泄漏问题
  4. 相同事件和函数无法只执行一次

由于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;
}

以上完毕!

  • weixiao kaixin tushetou jingkong deyi fanu liezui liuhan daku ganga bishi nanguo lihai qian yiwen numu tu yi haixiu se fadai minyan hehe henkaixin huaji biyiyan kuanghan maimeng shui xiaku penqi zhangzui pen aini ye niu laji ok chigua renshi kongbu shuai xiaoxiese touxiao huaixiao jingnu chihuai kaisang xiaoku koubi zhuangbi lianhong kanbujian shafa zhijing xiangjiao dabian yaowan redjing lazhu rizhi duocang chixigua hejiu xixi xiaopen goukun xiaobuchu shenme wusuowei guancha lajing chouyan xiaochi bie zhadanzui zhadanxiao