如何优雅的封装一个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;
            }
        }
    }
}

原文地址:https://www.cnblogs.com/yuliangbin/p/9460917.html

时间: 2024-10-06 11:34:25

如何优雅的封装一个DOM事件库的相关文章

动手封装一个滚轮事件吧!

/*滚轮事件函数封装*/ Object.prototype.onmousewheelhandlefun=function(handle){ var info=navigator.userAgent; var down=null; if(info.indexOf("Firefox")!=-1){ this.addEventListener("DOMMouseScrool",funciton(event){ var ev=event||window.event; if(

DOM事件简介

Click.touch.load.drag.change.input.error.risize — 这些都是冗长的DOM(文档对象模型)事件列表的一部分.事件可以在文档(Document)结构的任何部分被触发,触发者可以是用户操作,也可以是浏览器本身.事件并不是只是在一处被触发和终止:他们在整个document中流动,拥有它们自己的生命周期.而这个生命周期让DOM事件有更多的用途和可扩展性. 作为一个开发人员,我们必须要理解DOM事件是如何工作的,然后才能更好的驾驭它,利用它们潜在的优势,开发出

DOM事件阶段以及事件捕获与事件冒泡先后执行顺序

平时浏览这么多技术文章,如过不去实践.深入弄透它,这个技术点很快就会在脑海里模糊.要加深印象,就得好好过一遍.重要的事情说三遍,重要的知识写一遍. 开发过程中我们都希望使用别人成熟的框架,因为站在巨人的肩膀上会使得我们开发的效率大幅度提升.不过,我们也应该.必须了解其基本原理.比如DOM事件,jquery框架帮我们为我们封装和抽象了各浏览器的差异行为,为事件处理带来了极大的便利.不过浏览器逐步走向统一和标准化,我们可以更加安全地使用官方规范的接口.因为只有获得众多开发者的芳心,浏览器才会走得更远

整理之DOM事件阶段、冒泡与捕获、事件委托、ie事件和dom模型事件、鼠标事件

整理之DOM事件阶段 本文主要解决的问题: 事件流 DOM事件流的三个阶段 先理解流的概念 在现今的JavaScript中随处可见.比如说React中的单向数据流,Node中的流,又或是今天本文所讲的DOM事件流.都是流的一种生动体现.用术语说流是对输入输出设备的抽象.以程序的角度说,流是具有方向的数据. 事件流分事件冒泡与事件捕获 在浏览器发展的过程中,开发团队遇到了一个问题.那就是页面中的哪一部分拥有特定的事件? 可以想象画在一张纸上的一组同心圆,如果你把手指放在圆心上,那么你的手指指向的其

关于DOM 事件流的三个阶段

一丶 流 什么是流? 比如 react 中的单项数据流,Node.js 中的流,或者本文中的 DOM 事件流,都是流的具体体现.专业地讲,流是程序输入或输出的一个连续的字节序列:通俗地讲,流是有方向的数据. 二丶 事件流 什么是事件流? 假想一下,现在有一组同心圆,你把手指在最里面的圆心上,与此同时,你也正在指着外层的其他同心圆.假设最里面的圆是 DOM 中的一个按钮,那就是说,你点击按钮这个元素的同时,也点击了他的所有父级元素.那么这个点击事件 DOM 要怎么处理呢?事实上,这个点击事件并非只

利用epoll写一个&quot;迷你&quot;的网络事件库

epoll是linux下高性能的IO复用技术,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率.另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了.epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存I

随笔一个dom节点绑定事件

以下利用jquery说明: js中,给一个dom节点绑定事件再平常不过了.这里说下,如果dom经常发生变化的话,给这个dom绑定事件的情况. 比如代码如下: li的节点,绑定了事件:点击会打出来里头的html内容. button点击事件:会生成一个li节点. 1 <html> 2 <head> 3 <meta charset="UTF-8"> 4 </head> 5 <body> 6 <ul class="ul

C 封装一个通用链表 和 一个简单字符串开发库

引言 这里需要分享的是一个 简单字符串库和 链表的基库,代码也许用到特定技巧.有时候回想一下, 如果我读书的时候有人告诉我这些关于C开发的积淀, 那么会走的多直啊.刚参加工作的时候做桌面开发, 服务是C++写,界面是C#写.那时候刚进去评级我是中级,因为他问我关于系统锁和信号量都答出来.开发一段 时间,写C#也写的很溜.后面招我那个人让我转行就写C++和php,那时候就开始学习C++有关知识. 后面去四川工作了,开发安卓,用eclipse + java语法 + android jdk,开发前端,

C 封装一个简单二叉树基库

引文 今天分享一个喜欢佩服的伟人,应该算人类文明极大突破者.收藏过一张纸币类型如下 那我们继续科普一段关于他的简介 '高斯有些孤傲,但令人惊奇的是,他春风得意地度过了中产阶级的一生,而  没有遭受到冷酷现实的打击:这种打击常无情地加诸于每个脱离现实环境生活的  人.或许高斯讲求实效和追求完美的性格,有助于让他抓住生活中的简单现实.  高斯22岁获博士学位,25岁当选圣彼德堡科学院外籍院士,30岁任哥廷根大学数  学教授兼天文台台长.虽说高斯不喜欢浮华荣耀,但在他成名后的五十年间,这  些东西就像