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