JS 事件绑定(超大章)

52 0

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

以上完毕!

0
  • 本文分类:JavaScript
  • 本文标签:事件绑定
  • 流行热度:已超过 52 人围观了本文
  • 发布日期:2018年11月27日 - 20时48分00秒
  • 版权申明:本文系作者@木灵鱼儿原创发布在木灵鱼儿站点。未经许可,禁止转载。
微信收款码
微信收款码