如何优雅的封装一个DOM事件库

Posted 余大彬

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何优雅的封装一个DOM事件库相关的知识,希望对你有一定的参考价值。

1、DOM0级事件和DOM2级事件

DOM 0级事件是元素内的一个私有属性:div.onclick = function () {},对一个私有属性赋值(在该事件上绑定一个方法)。由此可知DOM 0级事件只能给元素的某一个行为绑定一次方法,第二次绑定会把前面的覆盖掉。

DOM 2级事件是让DOM元素通过原型链一直找到EventTarget这个内置类原型上的addEventListener方法来实现的。

DOM 2可以给某一个元素的同一个行为绑定多个不同的方法

//实例 1
obj.addEventListener(事件类型 , 处理函数 , false)
//IE9以下不兼容,可以为一个事件绑定多个处理程序,并且按照绑定时的顺序去执行
//实例2
div.addEventListener(‘click‘ , function f() {} , false); //1
div.addEventListener(‘click‘ , function f() {} , false); //2
//事件1和事件2虽然执行的函数一样,但是函数f()的地址不一样,所以是2个处理函数,执行2次
//实例3
function f() {};
div.addEventListener(‘click‘ , f , false); //1
div.addEventListener(‘click‘ , f , false); //2
//事件1和事件2执行的函数都是f(),但因为地址一样,所以只执行一次。即某一个元素的同一个行为只能绑定一次相同的方法

1.1、DOMContentLoaded和loaded

DOM 2还提供了DOM 0中没有的行为类型 -> DOMContentLoaded:当页面中的DOM结构(html结构)加载完成触发的行为。而onload事件则是当页面中的所有资源全部加载完成(图片、html结构、音视频...)才会被执行。

jQuery中的$(document).ready(function () {}),等价于$(function () {}),事件原理就是DOM2中新增的DOMContentLoaded事件。

1.2、事件的移除

DOM 0级事件的移除:div.onclick = null;

DOM 2级事件的移除:

function fn() {};
div.addEventListener(‘click‘,fn,false);
div.removeEventListener(‘click‘,fn,false);

1.3、DOM2事件机制

  • 只能给某个元素的同一个行为绑定多个"不同"的方法
  • 当行为触发,会按照绑定的先后顺序把绑定的方法执行
  • 执行的方法中的this是当前绑定事件的元素本身

1.4、IE6-8下的事件机制

在IE6-8浏览器中,不支持addEventListener/removeEventLiatener,如果想实现DOM 2事件绑定,只能用attachEvent(),移除用detachEvent()。

 obj.attachEvent(‘on‘+type , func);只能在冒泡阶段发生,一个事件同样可以绑定多个处理函数。与obj.addEventListener(‘type‘ , func , false)不一样的是,即使函数的地址是一样的,绑定多少次就执行多少次。即同一个函数可以绑定多次

与标准浏览器的事件池机制对比:

  • this问题:IE6-8中当方法执行的时候,方法中的this不是当前元素,而指的是window
  • 重复问题:可以给同一个元素的同一个行为绑定多个相同的方法
  • 顺序问题:执行的时候顺序是混乱的,标准浏览器是按照绑定顺序依次执行

2、处理this问题

/*
 *    bind: 处理DOM2级事件绑定的兼容性问题
 *    @parameter:
 *    curEle: 要绑定事件的元素
 *    eventType: 要绑定的事件类型(‘click‘,‘mouseover‘...)
 *    eventFn: 要绑定的方法
 */
 var tempFn = {};
 function bind(curEle,eventType,eventFn){
    if ("addEventListener" in document) {//标准浏览器
        curEle.addEventListener(eventType,eventFn,false);
        return ;
    }
    var tempFn[eventFn] = function () {
        eventFn.call(curEle);
    };
    curEle.attachEvent("on" + eventType,tempFn);
 }
 
 function unbind(curEle,eventType,eventFn) {
    if ("removeEventListener" in document) {
        curEle.removeEventListener(eventType,eventFn,false);
        return ;
    }
    curEle.detachEvent("on" + eventType,tempFn[eventFn]);
 }

//分析
 
// 1、知若想改变IE下事件执行函数的this的指向,可以在函数执行的时候改变this,即用函数eventFn.call(‘curEle‘),这样虽然解决了this指向的问题,
   但又抛出了一个新的问题:即不知道该如何移除该事件函数,因为绑定的是一个匿名函数,而匿名函数的地址我们是无法知道的。
  所以要先把匿名函数定义时的地址赋值给一个变量temp var tempFn = function () { eventFn.call(‘curEle‘); }; //2、为什么要把tempFn设置成一个全局变量 若tempFn不是一个全局变量,而是写在函数内部的私有变量,而私有变量只能在函数内部进行访问,
所以我们在bind()函数里的tempFn在unbind()函数里是不能访问的,因此也就不能移除该事件函数。所以若想移除该事件函数,tempFn就必须是全局变量

拓展:我们知道:写在函数内部的变量是私有变量,一个函数的私有变量只能在函数的内部进行访问。

  • 若在一个函数里要用到另一个函数里的变量,可以把该变量设置成全局的变量,这样两个函数都可以访问到。(这可能会造成全局污染)
  • 若几个函数的作用是为同一个/同一类元素提供方法去使用,且不同方法中要用到其它方法里的变量等,那么可以用该元素的自定义变量来存储这些变量,就可以实现在不同方法中的访问。(不会造成全局污染)

上面的代码除了全局变量可能造成污染外。还有一种不得不考虑的情况就是:当为不同的事件绑定方法时,不同的事件可能执行相同的方法(如mouseover 和 click 都执行fn1方法时),如果仍然将这些方法存储在一起,那么移除某一类事件的方法时就可能出错,因此我们需要为不同的事件创建不同的数组来存储绑定在其上的方法。

所以我们需要对代码进行进一步的优化。

function bind(curEle,eventType,eventFn){
        ...
        var tempFn = function () {
            eventFn.call(‘curEle‘);
        };
        tempFn.photo = eventFn;//给传入的每一个函数做一个唯一标识
        //首先判断该自定义属性之前是否存在,不存在的话创建一个,由于要存储多个方法,所以我们让其值是一个数组
        //为什么要对不同的事件类型创建不同的数组呢,因为不同的事件可能执行相同的方法。如mouseover 和 click 都执行fn1方法时,移除的时候就可能出错
        if (!curEle[‘bindFn‘ + eventType]) {
            curEle[‘bindFn‘ + eventType] = [];
        }
        curEle[‘bindFn‘ + eventType].push(tempFn);
        curEle.attachEvent("on" + eventType,tempFn);
    }
 
    function unbind(curEle,eventType,eventFn) {
        ...
        var arr = curEle[‘bindFn‘ + eventType];
        for (var i = 0; i < arr.length; i ++) {
            if (arr[i].photo === eventFn) {
                arr.splice(i,1);//找到后,把自己存储容器中对应的移除掉,与事件池中保持一致
                curEle.detachEvent("on" + eventType,arr[i]);//把事件池中对应的方法移除掉
                break;
            }
        }
    }

3、处理重复问题

function bind(curEle,eventType,eventFn){
        if ("addEventListener" in document) {
            //省略代码
        }
        //省略代码
        //处理重复问题:如果每一次往自定义属性添加方法前,看一下是否已经有了,有的话就不用重复添加,同理,也就不用往事件池里存储了
        var arr = curEle[‘bindFn‘ + eventType];
        for (var i = 0; i < arr.length ;i ++) {
            if (arr[i].photo === eventFn) {
                return ;
            }
        }
        arr.push(tempFn);
        curEle.attachEvent("on" + eventType,tempFn);
    }

4、处理顺序问题

我们知道在IE6-8下,事件的执行顺序是无序的,这是由浏览器的事件池机制所决定的。所以要改善这个问题,我们模仿标准浏览器的事件执行顺序,可以自己写一个事件池来使方法的执行顺序有序执行。听起来有点绕,我们来看一下具体的实现就清楚了。

  //创建自己的事件池,并把需要给当前元素绑定的方法依次增加到事件池中
    function on(curEle,eventType,eventFn) {
        if (!curEle[‘myEvent‘ + eventType]) {
            curEle[‘myEvent‘ + eventType] = [];
        }
        var arr = curEle[‘myEvent‘ + eventType];
        for (var i = 0; i < arr.length; i ++) {
            if (arr[i] === eventFn) return ;
        }
        arr.push(eventFn)
        bind(curEle,eventType,run);//把run方法绑定到自定义的bind()函数中,这个bind函数解决了this指向和重复问题。因此绑定后run方法的this指向当前点击元素
    }
 
   //在自己的事件池中把某一个方法移除
    function off(curEle,eventType,eventFn) {
        var arr = curEle[‘myEvent‘ + eventType];
        for (var i = 0; i < arr.length; i ++) {
            if (arr[i] === eventFn) {
                arr.splice(i,1);
            }
        }
    }
 
    //由于IE6-8浏览器DOM2级事件执行多个绑定方法时会出现顺序混乱,我们就只给它绑定一个run方法,然后在run方法里执行事件池on里绑定的方法。
    function run(event) {
        event = event || window.event;
        var flag = event.target ? true :false ;//IE6-8下不兼容event.target
        if (!flag) {//做非兼容处理
            event.target = window.srcElement;
            event.pageX = event.clientX + document.documentElement.scrollLeft;
            event.pageY = event.clentY +document.documentElement.scrollTop;
            event.preventDefault = function () {
                event.returnValue = false ;
            }
            event.stopPropagation = function () {
                event.cancleBubble = true ;
            }
        }
        //获取事件池中绑定的方法,并且让这些方法依次执行
        var arr = event.target[‘myEvent‘ + event.type];
        for (var i = 0; i < arr.length; i ++) {
            arr[i].call(event.target,event);//把事件对象传递给当前执行的函数
        }    
    }

5、一个完整的DOM库

以上就是对封装整个DOM库的思考,可见分析的整个过程是多么的煎熬。然而整个的DOM库封装后,代码却少的可怜。我们一起来看一下。

//绑定事件
function
on(ele,type,fn) { if(ele.addEventListener) { ele.addEventListener(type,fn,false); } else{ if (!ele[‘myEvent‘ + type]) { ele[‘myEvent‘ + type] = []; ele.attachEvent(‘on‘ + type,function(){//在这里绑定run方法 run.call(ele); }) } let arr = ele[‘myEvent‘ + type]; for(let i = 0; i < arr.length; i++) { if (arr[i] == fn) { return; } } arr.push(fn); } }
//解决IE下事件执行顺序的run方法
function run() { let e = window.event;//在IE6-8下,事件对象是存储在全局的event属性上的 e.target = e.srcElement; e.preventDefault = function () { e.returnValue = false ; } e.stopPropagation = function () { e.cancleBubble = true ; } let arr = this[‘myEvent‘ + event.type]; for(let i = 0; i < arr.length; i++) { if(arr[i] == null) {//在这里删除被解绑的方法 arr.splice(i,1); i--; } arr[i].call(this,event); } }
//解除事件
function off(ele,type,fn) { if (ele.removeEventListener) { ele.removeEventListener(type,fn,false); } else { let arr = ele["myEvent" + type]; for(let i = 0; i < arr.length; i++) { if (arr[i] == fn) { arr[i] = null; //arr.splice(i,1);这里为什么不能直接删除掉,而是要用null来占位。答案是:为了不改变arr的长度。使run能正确执行。 return; } } } }

以上是关于如何优雅的封装一个DOM事件库的主要内容,如果未能解决你的问题,请参考以下文章

使用 JQuery ajax 在 DOM 操作后附加事件

Reactreact概述组件事件

jquer 事件,选择器,dom操作

zBase --轻量级DOM操作库

JavaScript进阶:如何写出优雅的JavaScript代码

属性.native用于解决第三方el组件库@click事件无效