DOM0事件和DOM2事件模型 —— 2、DOM2级事件核心原理和兼容处理

1、初步了解js中事件绑定的方式

DOM0事件绑定

1.oBox.onclick=function(e){
2.    //=>this:oBox
3.    e=e||window.event;
4.}
5.oBox.onmouseenter=function(){}
6....
复制代码

DOM2事件绑定

// 标准浏览器
oBox.addEventListener('click',function(e){
    // this:oBox
    // e:事件对象
},false)
// false:让当前绑定的方法在冒泡传播阶段执行(一般都用false)
// true:让当前绑定的方法在捕获阶段执行(一般不用)



// IE6~8
oBox.attachEvent('onclick',function(e){

    // e:事件对象,不同于DOM0事件绑定,使用attachEvent绑定方法,当事件触发方法执行的时候,浏览器也会
    // 把事件对象当做实参传递给函数(传递的值和window.event是相同的),所以IE6~8下获取的事件对象对于:
    // pageX/pageY/target...依然没有,还是存在兼容性(事件对象兼容处理在DOM2中依然存在)

});
//=> 此时绑定的方法都是在冒泡传播阶段执行
复制代码

有DOM0和DOM2事件绑定,那么DOM1事件绑定呢?

因为在DOM第一代升级迭代的时候,DOM元素的事件绑定方式依然沿用的是DOM0代绑定的方式,在此版本DOM中,事件绑定没有升级处理

在DOM2代的时候才升级处理,所以把DOM2代升级处理的事件方式叫DOM2事件绑定。

2、DOM0事件绑定的核心原理

DOM0事件绑定和DOM2事件绑定的区别

DOM0事件绑定的原理

1、给当前元素对象的某一个私有属性(onxxx这样的私有属性)赋值的过程(之前属性默认值是null,如果我们给赋值一个函数,相当于绑定了一个方法)

2、当我们赋值成功(赋值一个函数),此时浏览器会把DOM元素和赋值的函数建立关联,以及建立DOM元素行为操作的监听,当某一个行为被用户触发,浏览器会把相关行为赋值的函数执行

特点:

1.//=>1、只有DOM元素天生拥有这个私有属性(onxxx事件私有属性),我们赋值的方法才叫做事件绑定,否则属于给当前元素设置一个自定义属性而已
2.document.body.onclick=function(){}//->事件绑定
3./*
4. * 手动点击页面中的BODY触发方法执行
5. * document.body.onclick() 这样执行也可以
6.*/
7.
8.document.body.onzhufeng=function(){}//->设置自定义属性
9./*
10. * 只能document.body.onzhufeng()这样执行
11. */
12.
13.//=>2、移除事件绑定的时候,我们只需要赋值为null即可
14.document.body.onclick=null;
15.
16.//=>3、在DOM0事件绑定中,只能给当前元素的某一个事件行为(某一个事件私有属性)绑定一个方法,绑定多个方法,最后一次绑定的会把之前绑定的都替换掉
17.document.body.onclick=function(){
18.    console.log(1);
19.}
20.document.body.onclick=function(){
21.    console.log(2);
22.}
23.//=>点击BODY只能输出2,因为第二次赋值的函数把第一次赋值的函数给替换了
复制代码

原理:给当前元素对象的某一个私有属性(onxxx这样的私有属性)赋值

特点:只能给当前元素的某一个事件行为(某一个事件私有属性)绑定一个方法,因为一个私有属性只能绑定一个方法

3、DOM2事件绑定的核心原理

DOM2事件绑定的原理

当前这个实例document.body之所以能用addEventListener这个方法,说明这个方法肯定在它的原型上出现。

谁能用addEventListener就看谁是不是EventTarget这个内置类的实例(可以用instanceo检测:比如 window instanceof EventTarget)

1、我们DOM2事件绑定使用的addEventListener/attachEvent都是在EventTarget这个内置类的原型上定义的,我们调取使用的时候,首先通过原型链找到这个方法,然后执行完成事件绑定的效果

2、浏览器首先会给当前元素的某一个事件行为开辟一个事件池(事件队列)[其实是浏览器有一个统一的事件池,我们每个元素的某个行为绑定的方法都放在这个事件池中,只是通过相关的标识来区分的],当我们通过addEventListener做事件监听的时候,会把绑定的方法存放在事件池中

3、当元素的某一个行为触发,浏览器会到对应的事件池中,把当前存放在事件池中的所有方法,依次按照存放的先后顺序执行

DOM0是私有属性赋值,DOM2是事件池的事件机制

DOM2事件绑定的特点:

//=>1、所有DOM0支持的事件行为,DOM2都可以使用,不仅如此,DOM2还支持一些DOM0没有的事件行为:DOMContentLoaded...

window.onDOMContentLoaded === undefined; //=>DOM0中没有这个属性

window.addEventListener('DOMContentLoaded',function(){
    //=>标准浏览器中兼容这个事件:当浏览器中的DOM结构加载完成,就会触发这个事件(也会把绑定的方法执行)
},false);

window.attachEvent('onDOMContentLoaded',function(){
    //=>IE6~8中的DOM2也不支持这个事件
});

//=>2、DOM2中可以给当前元素的某一个事件行为绑定‘多个不同的方法’(不同的方法=>不同的空间地址,比如一个私有fn和全局的fn地址不一样,所以不是同一个fn方法,)(因为绑定的所有方法都存放在事件池中了;而DOM0是通过给私有属性赋值来绑定方法的,一个私有属性只能附一个值)
function fn1(){
    console.log(1);
}
function fn2(){
    console.log(2);
}
function fn3(){
    console.log(3);
}
document.body.addEventListener('click',fn1,false);
document.body.addEventListener('click',fn3,false);
document.body.addEventListener('click',fn2,false);
document.body.addEventListener('click',fn3,false);//=>本次向事件池中存储的时候,发现fn3已经在事件池中存在了,不能再存储了。

//3、DOM2事件绑定的移除比较麻烦一些,需要和绑定的时候:事件类型、绑定的方法、传播阶段,三个完全一致才可以移除掉
document.body.removeEventListener('click',fn2,false);

document.body.addEventListener('click',function(){
    console.log(1);
},false);

document.body.removeEventListener('click',function(){
    console.log(1);
},false);
//=>DOM2事件绑定需要我们养成‘未雨绸缪’的能力,绑定方法的时候尽量不用匿名函数,为后期可能会把方法在事件池中移除掉做准备

//4、例子
    var obj = {
        fn1: function () {
            console.log(1);
        }//这是一个地址
    };
    document.body.addEventListener('click',obj.fn1,false);
    obj.fn1 = function () {//这是一个新的地址
        console.log(2);
    };
    document.body.addEventListener('click',obj.fn1,false);

    // 输出结果是 1/2
    // 首先不同的方法不是按方法名来区分,而是按空间地址来区分的,只要空间地址不同方法就不同。js中所有
    // 的数据类型操作都是按堆栈类型操作的

复制代码

DOM0和DOM2的核心原理本质区别:

DOM0是给私有属性赋值,DOM2中可以给当前元素的某一个事件行为的事件池增加方法。

DOM0只能给当前元素的某个事件行为绑定一个方法,绑定多个也是以最后一个为准;DOM2可以在事件池里面随便放很多不同的方法,就可以绑定多个。

DOM0当有内置属性才能做事件绑定,其他属于自定义属性赋值;DOM2支持所有DOM0支持的事件行为,还支持一些DOM0没有的事件行为:DOMContentLoaded...

DOM0的移除是直接赋值为null;DOM2事件绑定需要我们养成‘未雨绸缪’的能力,绑定方法的时候尽量不用匿名函数,为后期可能会把方法在事件池中移除掉做准备

4、关于DOM事件原理的一些扩充




    "UTF-8">
    
    "stylesheet" href="css/reset.min.css">
    






复制代码

5、window.onload(document.onload)和document.ready的区别

window.onload:当浏览器中所有的资源内容(DOM结构、文本内容、图片…)都加载完成,触发load事件 
1、它是基于DOM0事件绑定完成的,所以在同一个页面中只能给它绑定一个方法(绑定多个也以最后一个绑定的为主)

2、如果想在一个页面中使用多次,我们应该是基于DOM2事件绑定的

1.function fn1(){
2.    //=>第一件事情
3.}
4.function fn2(){
5.    //=>第二件事情
6.}
7.window.addEventListener('load',fn1,false);
8.window.addEventListener('load',fn2,false);
9....
$(function(){}) 或者 $(document).ready(function(){}) 
当文档中的DOM结构加载完成就会被触发执行,而且在同一个页面中可以使用多次

1、JQ中提供的方法,JQ是基于DOMContentLoaded这个事件完成这个操作的 
2、JQ中的事件绑定都是基于DOM2事件绑定完成的 
3、但是DOMContentLoaded在IE6~8下使用attachEvent也是不支持的,JQ在IE6~8中使用的是readystatechange这个事件处理的
复制代码

window.onload(document.onload)和document.ready的区别是什么?

window.onload只能执行一次,当浏览器中所有的资源内容都加载完成后才执行,jQ中的document.ready在页面中可以使用多次,而且是DOM结构加载完成就会被触发执行

之前深入研究了下:window.onload是基于DOM0事件绑定的所以只能用一次,如果想在一个页面中使用多次我们可以用DOM2通过addEventListener和attachEvent就可以解决了(把会DOM2暴露出来,面试官可能会对DOM2感兴趣,可能会基于DOM2问你问题)

jq源码也看过一部分,jq是新增加DOMContentLoaded这个事件完成的,所以说它在标准浏览器当中如果支持DOMContentLoaded这个事件的话,她肯定是在dom结构加载完成之后就执行了了,但是DOMContentLoaded在IE6~8下使用attachEvent也是不支持的,用的readystatechange这个事件处理的。

jq所有事件都是基于DOM2来完成的,所以能在页面中使用多次,因为都把方法放在事件池当中

这样答,是站在新的高度上回答的。(这里面提出了两个东西面试官会感兴趣:dom2和dom2的事件池,关于jq的源码)项目不忙的时候,想把js提高下,读过部分jq源码

jq源码都写过:

jq是一个类,整个构建基于一个类,原型上扩展方法和把jq当初普通对象去扩展方法,extend扩展方法都已经写过了

封装的很多css方法,DOM库封装,offset方法这些方法都是仿照jq源码封装的(这些常用的方法和兼容出来都仿照jq来实现的)

$使用权转让noConflict(见jQuery源码解读及插件封装的5、jQ中的常用的方法(对照API文档过一遍)) 基于闭包,怎么window.$和window.jQuey...这些东西随便拿出两点聊聊就很可以了

jq这个坑挖出来就可以填进去(多看看之前写的jq的源码方面的知识点)

对于DOM2级事件,可能会问ie和火狐之下有什么区别?

事件对象的区别,ie是window.enevt获取的不是传进来的,标准浏览器是传进来的,所以说有些属性不一样(鼠标里面的话像target不兼容,pagex、pagey不兼容,preventDefault阻止事件的默认行为和stopPropagation阻止事件的冒泡传播这些常用的属性不兼容,键盘事件对象which不兼容IE6~8,我们使用keyCode来实现)

之前公司做的拖拽的一个项目,拖拽的时候经常会出现鼠标丢失这个问题,在谷歌下可以绑定给document来解决,ie和火狐下可以使用setCapture来处理,但是谷歌不兼容。所以一般使用document

DOM2标准浏览器用addEventListener绑定和移除,IE6~8用attachEvent绑定和移除

DOM2事件绑定的时候语法上的区别:

在IE6~8中使用attachEvent做事件绑定(把方法存放在当前元素指定事件类型的事件池中)

1、顺序问题:当事件行为触发,执行对应事件池中存放方法的时候,IE低版本浏览器执行方法的顺序是乱序(标准浏览器是按照绑定的先后顺序依次执行的)

2、重复问题:IE低版本浏览器在向事件池中增加方法的时候,没有去重机制,哪怕当前方法已经存放过了,还会重复的添加进去(标准浏览器的事件池机制很完善,可以自动去重:已经存在过的方法不允许在添加进来)

3、THIS问题:IE低版本浏览器中,当事件行为触发,把事件池中方法执行,此时方法中的this指向window,而不是像标准浏览器一样,指向当前元素本身

究其根本:其实都是IE低版本浏览器对于它内置事件池处理机制的不完善导致的

DOM2事件绑定兼容处理的原理:告别LOW的IE6~8的内置事件池,而是自己创建一个类似于标准浏览器的“自定义事件池”,标准浏览器不需要处理兼容,只有IE6~8中才需要处理兼容

**接下来可能会问 那DOM2级事件绑定的兼容问题怎么处理?**接下来会写

6、DOM2兼容处理(语法上的兼容处理)

语法上的兼容处理

[标准]

curEle.addEventListener(‘type’,fn,false);

[IE6~8]

curEle.attachEvent(‘ontype’,fn);

2-1.js

/*
 * on:基于DOM2实现事件绑定(兼容所有浏览器)
 * 
 * @parameter
 *  curEle:当前需要操作的元素
 *  type:需要绑定方法的事件类型
 *  fn:需要绑定的方法
 *
 * @return
 *  不需要返回值
 */
//=>ON:给当前元素的某个事件绑定某个方法
var on = function (curEle, type, fn) {
    if (document.addEventListener) {
        //=>标准浏览器
        curEle.addEventListener(type, fn, false);
        return;
    }
    //=>IE6~8
    curEle.attachEvent('on' + type, fn);
};

//=>OFF:移除当前元素某个事件绑定的某个方法
var off = function (curEle, type, fn) {
    if (document.removeEventListener) {
        curEle.removeEventListener(type, fn, false);
        return;
    }
    //=>IE6~8
    curEle.detachEvent('on' + type, fn);
};
复制代码

2-DOM2.html




    "UTF-8">
    珠峰培训
    "stylesheet" href="css/reset.min.css">
    







复制代码

7、DOM2兼容处理(三大区别和总体概述)

除了语法上的区别,在处理的机制上有一些区别

在IE6~8中使用attachEvent做事件绑定的一些问题(把方法存放在当前元素指定事件类型的事件池中)

2-1.js

/*
 * on:基于DOM2实现事件绑定(兼容所有浏览器)
 * 
 * @parameter
 *  curEle:当前需要操作的元素
 *  type:需要绑定方法的事件类型
 *  fn:需要绑定的方法
 *
 * @return
 *  不需要返回值
 */
//=>ON:给当前元素的某个事件绑定某个方法
var on = function (curEle, type, fn) {
    if (document.addEventListener) {
        //=>标准浏览器
        curEle.addEventListener(type, fn, false);
        return;
    }
    //=>IE6~8
    curEle.attachEvent('on' + type, fn);
};

//=>OFF:移除当前元素某个事件绑定的某个方法
var off = function (curEle, type, fn) {
    if (document.removeEventListener) {
        curEle.removeEventListener(type, fn, false);
        return;
    }
    //=>IE6~8
    curEle.detachEvent('on' + type, fn);
};
复制代码

2-DOM2.html




    "UTF-8">
    珠峰培训
    "stylesheet" href="css/reset.min.css">
    







复制代码

1、顺序问题:当事件行为触发,执行对应事件池中存放方法的时候,IE低版本浏览器执行方法的顺序是乱序(标准浏览器是按照绑定的先后顺序依次执行的)

2、重复问题:IE低版本浏览器在向事件池中增加方法的时候,没有去重机制,哪怕当前方法已经存放过了,还会重复的添加进去(标准浏览器的事件池机制很完善,可以自动去重:已经存在过的方法不允许在添加进来)

3、THIS问题:IE低版本浏览器中,当事件行为触发,把事件池中方法执行,此时方法中的this指向window,而不是像标准浏览器一样,指向当前元素本身

究其根本:其实都是IE低版本浏览器对于它内置事件池处理机制的不完善导致的

DOM2事件绑定兼容处理的原理:告别LOW的IE6~8的内置事件池,而是自己创建一个类似于标准浏览器的“自定义事件池”,标准浏览器不需要处理兼容,只有IE6~8中才需要处理兼容

8、DOM2兼容处理(处理兼容的核心思想)

9、DOM2兼容处理(初步处理DOM2兼容)

2-DOM2.html




    "UTF-8">
    珠峰培训
    "stylesheet" href="css/reset.min.css">
    







复制代码

2-2.js 思路来自 => 8、DOM2兼容处理(处理兼容的核心思想)

//=>ON:给当前元素的某个事件绑定某个方法
var on = function (curEle, type, fn) {

    if (document.addEventListener) {//标准浏览器下执行内置的addEventListener
        curEle.addEventListener(type, fn, false);
        return;
    }

    //=>创建自定义事件池:没有才去创建(创建到当前元素的自定义属性上,以后在其它的方法中需要使用这个事件池,直接获取使用即可,不受作用域的限制。如果这里用变量var ary = [],其他地方不能使用,除非是全局变量,全局变量容易污染)
    //1)每一个事件应该有一个自己独有的自定义事件池,防止事件之间的冲突(通过事件来区分)
    if (typeof curEle[type + 'Pond'] === 'undefined') {//这里的代码只执行一次
        curEle[type + 'Pond'] = [];
        //=>只要执行ON就说明当前元素的这个事件行为将要被触发,我们需要绑定方法,此时我们应该把RUN先放在内置的事件池中(当行为触发的时候,先执行RUN,在RUN中在把我们自定义事件池中的方法执行)
        // curEle.attachEvent('on' + type, run);//=>把RUN只能向内置事件池中存放一次,只存run这个方法,不能再存别的方法并且只存一次(所以代码写在这),这样写run方法中的this是window,不是当前元素(因为attachEvent中THIS问题),但是通过内置方法attachEvent来绑定run所以会给run方法传递事件对象e
        curEle.attachEvent('on' + type, function (e) {
            run.call(curEle, e);//改变this指向,这里不能直接run.bind()改变,因为bind本身不兼容ie6~8,所以写个回调函数再通过call来改变this指向
        });//=>把RUN只能向内置事件池中存放一次(所以代码写在这)
    }
    var ary = curEle[type + 'Pond'];
    //=>去重操作:增加之前首先看一下当前自定义事件池中是否有这个方法,有这个方法,我们就不增加了
    for (var i = 0; i < ary.length; i++) {
        if (ary[i] === fn) {
            return;
        }
    }
    //=>向自定义的事件池中增加方法
    ary.push(fn);

};

//=>OFF:移除当前元素某个事件绑定的某个方法
var off = function (curEle, type, fn) {
    if (document.removeEventListener) {//标准浏览器下执行内置的removeEventListener
        curEle.removeEventListener(type, fn, false);
        return;
    }

    //=>从自定义事件池中把某个方法移除
    var ary = curEle[type + 'Pond'];
    if (ary) {
        for (var i = 0; i < ary.length; i++) {
            if (ary[i] === fn) {
                //=>这一项就是想移除的
                ary.splice(i, 1);
                break;
            }
        }
    }

};

//=>RUN:把自定义事件池中存放的方法依次执行(并且处理THIS等问题)
var run = function (e) {//接收事件对象e 
    // var ary = curEle[e.type + 'Pond'];//通过e.type获取当前触发行为的事件类型,但是curEle无法获取,如果能让run方法中的this变成当前元素
    //=>this:curEle
    var ary = this[e.type + 'Pond'];
    if (ary) {
        for (var i = 0; i < ary.length; i++) {
            var item = ary[i];
            item.call(this, e);//让this变成当前元素(这里的this就是curEle当前元素) 并把e传递给这个方法
        }
    }
};

// 以上解决了重复问题,顺序问题,this问题
复制代码

10、DOM2兼容处理(复习加事件对象的兼容处理)

1-event.html




    "UTF-8">
    珠峰培训
    "stylesheet" href="css/reset.min.css">
    







复制代码

1-1.js

var fn1 = function (e) {
    // console.log(1, this, e.target);
    console.log(1, this, e.xxx);
};
var fn2 = function (e) {
    console.log(2);
};
var fn3 = function (e) {
    console.log(3);
};
var fn4 = function (e) {
    console.log(4);
};
var fn5 = function (e) {
    console.log(5);
};
var fn6 = function (e) {
    console.log(6);
};
var fn7 = function (e) {
    console.log(7);
};
var fn8 = function (e) {
    console.log(8);
};
var fn9 = function (e) {
    console.log(9);
};
var fn10 = function (e) {
    console.log(10);
};
var fn11 = function (e) {
    console.log(11);
};
var fn12 = function (e) {
    console.log(12);
};

on(document.body, 'click', fn1);
on(document.body, 'click', fn2);
on(document.body, 'click', fn3);
on(document.body, 'click', fn4);
on(document.body, 'click', fn5);
on(document.body, 'click', fn6);
on(document.body, 'click', fn7);
on(document.body, 'click', fn8);
on(document.body, 'click', fn9);
on(document.body, 'click', fn10);
on(document.body, 'click', fn11);
on(document.body, 'click', fn12);
on(document.body, 'click', fn12);
on(document.body, 'click', fn12);
on(document.body, 'click', fn12);
复制代码

event-back1.js

//=>DOM2事件绑定原理:
//1、当我们通过addEventListener/attachEvent可以把需要执行的方法存放到当前元素某一个事件类型对应的内置事件池中
//2、当用户触发当前元素某个事件类型的时候,浏览器会自动找到对应的内置事件池,并且把事件池中存放的方法依次执行

//=>IE6~8事件池的机制  VS  标准浏览器事件池的机制
//1、浏览器执行事件池中方法的时候,不仅把方法执行,而且还把事件对象作为实参传递给对应的方法,但是也是有区别的:IE6~8中传递的事件对象和window.event是相同的,和标准浏览器中传递的事件对象是存在兼容的
//2、向事件池中存放方法的时候,IE(指的是IE低版本)的事件池没有去重机制,而标准(指标准浏览器)事件池中可以自动去重:之前存放过的方法,不允许二次存放
//3、当行为触发,浏览器执行事件池中方法的时候,IE事件池中的方法是乱序执行的,而标准事件池中的方法会按照存放时候的先后顺序依次执行
//4、执行事件池中的方法,IE低版本浏览器让方法中的THIS指向的是WINDOW,标准浏览器让方法中的THIS指向的是当前被操作的元素

//=====================================

//=>ON:向事件池中追加方法
function on(curEle, type, fn) {
    if (typeof curEle['pond' + type] === 'undefined') {
        curEle['pond' + type] = [];
        //->把RUN放到内置事件池中
        curEle.addEventListener(type, run, false);//=>应该是IE6~8中才需要这样处理,所以使用ATTACH-EVENT绑定才可以
        //1、RUN中的THIS:CUE-ELE
        //2、RUN中传递了事件对象(E),和WINDOW.EVENT相同
    }
    var pondAry = curEle['pond' + type];
    for (var i = 0; i < pondAry.length; i++) {
        if (pondAry[i] === fn) {
            return;
        }
    }
    pondAry.push(fn);
}
//=>OFF:移除事件池中的某个方法
function off(curEle, type, fn) {
    var pondAry = curEle['pond' + type];
    if (!pondAry) return;
    for (var i = 0; i < pondAry.length; i++) {
        if (pondAry[i] === fn) {
            pondAry.splice(i, 1); //=>会引发数组塌陷,我们删除的时候不能让原始数组中的索引改变
            
            break;
        }
    }
}
//=>RUN:把自己造的假事件池中的方法依次执行
function run(e) {
    //=>THIS:CUR-ELE
    //=>E:WINDOW-EVENT
    //=>把事件对象E的兼容处理好:把绑定方法执行的时候,我们把处理好的E传递给方法,以后在绑定的方法中直接用即可,不用在考虑兼容问题了(类似于JQ中事件绑定中的E)
    if (typeof e.target === 'undefined') {
        e.target = e.srcElement;
        e.which = e.keyCode;
        e.pageX = e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft);
        e.pageY = e.clientY + (document.documentElement.scrollTop || document.body.scrollTop);
        e.preventDefault = function () {
            e.returnValue = false;
        };
        e.stopPropagation = function () {
            e.cancelBubble = true;
        };
    }
    e.xxx = 'xxx';//测试标准浏览器下run里面事件对象有没有处理到 能不能输出这个xxx  能输出就处理到了
    var pondAry = this['pond' + e.type];
    if (!pondAry) return;
    for (var i = 0; i < pondAry.length; i++) {
        var itemFn = pondAry[i];
        //itemFn();//=>THIS:WINDOW E:UNDEFINED
        
        
        
        
        
        
        itemFn.call(this, e);
    }
}
复制代码

11、DOM2兼容处理(解决移除时候的数组的塌陷问题)

10解决了事件对象的兼容,但是自己定义的假事件池中通过pondAry.splice(i, 1),会引发数组塌陷,我们删除的时候不能让原始数组中的索引改变,比如如果在fn4中移除假事件池中的fn1、2、3,点击页面会出现以下bug

1-1.js

var fn1 = function (e) {
    console.log(1);
};
var fn2 = function (e) {
    console.log(2);
};
var fn3 = function (e) {
    console.log(3);
};
var fn4 = function (e) {
    console.log(4);
    off(document.body, 'click', fn1);
    off(document.body, 'click', fn2);
    off(document.body, 'click', fn3);
};
var fn5 = function (e) {
    console.log(5);
};
var fn6 = function (e) {
    console.log(6);
};
var fn7 = function (e) {
    console.log(7);
};
var fn8 = function (e) {
    console.log(8);
};
var fn9 = function (e) {
    console.log(9);
};
var fn10 = function (e) {
    console.log(10);
};
var fn11 = function (e) {
    console.log(11);
};
var fn12 = function (e) {
    console.log(12);
};

on(document.body, 'click', fn1);
on(document.body, 'click', fn2);
on(document.body, 'click', fn3);
on(document.body, 'click', fn4);
on(document.body, 'click', fn5);
on(document.body, 'click', fn6);
on(document.body, 'click', fn7);
on(document.body, 'click', fn8);
on(document.body, 'click', fn9);
on(document.body, 'click', fn10);
on(document.body, 'click', fn11);
on(document.body, 'click', fn12);
on(document.body, 'click', fn12);
on(document.body, 'click', fn12);
on(document.body, 'click', fn12);
复制代码

第一次点击页面输出

第二次点击页面输出

这是为什么呢?如图:

解决(参照10:event-back1.js的注释部分代码)

解决数组塌陷的两种形式的编程思想

第一种形式:当前这个循环删除某一项后,如果i++就会跳过这一项,这种数组塌陷就让i--

第二种形式:循环一个数组循环到一半的时候,在另外一个地方把这个数组改了,也会导致数组塌陷(因为同一个数组在同一个空间,会受影响),一定要保证当前数组 在循环过程当中它的索引不能改变,不能在其他地方把索引改了。此时不能再另外一个地方把这个数组改了,只能让不需要的那一项为null,下一次再循环数组的时候再 把为null的删除掉。

这是最常用的解决思路,还有其它思路如下:

比如在删除的时候,返回一个标识表示这一项已经删除了,返回值是删除的哪几个,再继续执行的时候,比如执行到fn4的时候接受到这个值让索引不改变,但是更麻烦,

还有一种思路:每次执行run首先把数组克隆,循环克隆的数组,off里面删除原始的数组,第二次执行的时候先把原始的拿来克隆一份(第二次执行用的是最新的原始的)

12、DOM2兼容处理(初步完善)

之前参照10中的event-back1.js在on方法中使用的标准浏览器下addEventListener把run方法放进内置事件池中,现在改用ATTACH-EVENT兼容IE6~8

//=>ON:向事件池中追加方法
function on(curEle, type, fn) {

    if (document.addEventListener) {//标准浏览器不用处理兼容
        curEle.addEventListener(type, fn, false);
        return;
    }

    // 以下兼容ie6~8造的假事件池
    if (typeof curEle['pond' + type] === 'undefined') {
        curEle['pond' + type] = [];

        // 只要执行ON就说明当前元素的这个事件行为将要被触发,我们需要绑定方法,此时我们应该把RUN先放在内置的事件池中
        //(当行为触发的时候,先执行RUN,在RUN中在把我们自定义事件池中的方法执行)参照9的2-2.js注解
        // -------------------------------------------------------------------------------------------------------


        //   //->把RUN放到内置事件池中
        //   curEle.addEventListener(type, run, false);//=>应该是IE6~8中才需要这样处理,所以使用ATTACH-EVENT绑定才可以
        //   //1、RUN中的THIS:CUE-ELE
        //   //2、RUN中传递了事件对象(E),和WINDOW.EVENT相同


        // ---------------------------------------------------------------------------------------------------------
        //-----最开始用的addEventListener标准浏览器下的写法吧run方法放进内置事件池中,现在改用ATTACH-EVENT兼容IE6~8----
        // ---------------------------------------------------------------------------------------------------------


        // curEle.attachEvent('on' + type, run);
        // //ie6~8下往内置事件池中放run用attachEvent,type也不能直接写出type,需要加on,并且this不能保证是当前元素
        // // 需要保证run中的this是当前元素,在ie6~8下通过attachEvent绑定,run中的this会指向window(执行run也需要
        // // 传递一个事件对象进来,在ie6~8下run本来是有事件对象的但是解决this过程中run中不一定有事件对象了)


        // // 有一个非常简单的方式可以解决:用DOM0级解决(就是想把run放在内置事件池,点击的时候触发run执行,除了DOM2事件池
        // // 机制,点击的时候触发,DOM0级也可以实现)
        // curEle['on' + type] = run;//DOM0级事件绑定,run方法中的this就是curEle,但是这样做run方法中事件对象e没了,
        // // 需要在run方法中加 e = e||window.event 解决;但是DOM0级事件绑定容易被外面写的方法替换掉,因为DOM0级事件绑定
        // // 只能绑定一个方法,
        

        // 最笨的解决方法,内置里面放个匿名函数,这个匿名函数里面有事件对象e,当去触发这个操作的时候执行匿名函数,在匿名函
        // 数执行的时候再强制让run执行,但是要让run里面的this变成当前元素curEle,并且传递事件对象e
        curEle.attachEvent('on' + type, function (e) {
            run.call(curEle, e);
        });
        // 这里的匿名函数不能用箭头函数,首先on方法中的this是window,箭头函数中的this是宿主环境中的this,所以这里用箭头函数this还是
        // 是window,并且箭头函数ie不支持。

    }
    var pondAry = curEle['pond' + type];
    for (var i = 0; i < pondAry.length; i++) {
        if (pondAry[i] === fn) {
            return;
        }
    }
    pondAry.push(fn);
}


//=>OFF:移除事件池中的某个方法
function off(curEle, type, fn) {

    if (document.removeEventListener) {//标准浏览器不用处理兼容
        curEle.removeEventListener(type, fn, false);
        return;
    }

    // 以下兼容ie6~8造的假事件池
    var pondAry = curEle['pond' + type];
    if (!pondAry) return;
    for (var i = 0; i < pondAry.length; i++) {
        if (pondAry[i] === fn) {
            // pondAry.splice(i, 1); //=>会引发数组塌陷,我们删除的时候不能让原始数组中的索引改变
            pondAry[i] = null;//用null占位,这项没有了但是位置还在,索引不会变,但是第二次执行的时候,null不能执行,所以run里面需要做判断
            break;
        }
    }
}


//=>RUN:把自己造的假事件池中的方法依次执行
function run(e) {
    //=>THIS:CUR-ELE
    //=>E:WINDOW-EVENT
    //=>把事件对象E的兼容处理好:把绑定方法执行的时候,我们把处理好的E传递给方法,以后在绑定的方法中直接用即可,不用在考虑兼容问题了(类似于JQ中事件绑定中的E)
    if (typeof e.target === 'undefined') {
        e.target = e.srcElement;
        e.which = e.keyCode;
        e.pageX = e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft);
        e.pageY = e.clientY + (document.documentElement.scrollTop || document.body.scrollTop);
        e.preventDefault = function () {
            e.returnValue = false;
        };
        e.stopPropagation = function () {
            e.cancelBubble = true;
        };
    }
    var pondAry = this['pond' + e.type];
    if (!pondAry) return;
    for (var i = 0; i < pondAry.length; i++) {
        var itemFn = pondAry[i];
        //itemFn();//=>THIS:WINDOW E:UNDEFINED
        if (itemFn === null) {
            //=>当前这一项在执行的过程中被OFF方法移除掉了(NULL不能执行,执行会报错),第二次执行开始删除为null的项
            pondAry.splice(i, 1);
            i--;
            continue;
        }
        itemFn.call(this, e);
    }
}
复制代码

ie6~8需要处理DOM2事件绑定的兼容

on方法就是自己先构造一个假的事件池,把所有需要绑的方法放在假的事件池中,放的时候通过循环去重

执行on方法还需要做一件事情:执行on方法说明想给当前元素的这个事件类型绑定方法,点击的时候首先要去执行的是run方法,所以需要把run方法放到内置事件池当中去,最终点击的时候要执行的是run,在run中把这些绑定的方法再执行,

off就是从假的事件池中把不需要的拿出来,为了防止数组塌陷,先让不需要的项变成null,不改变索引值,

run方法很重要,首先要把唯一的方法run放在内置的事件池当中,为了解决this问题放一个匿名函数,执行匿名函数的时候执行run,也相当于把run放在内置事件池当中了,并且通过 run.call(curEle, e)让this指向当前元素并且传递事件对象e

run方法很重要,当行为触发的时候首先先执行run,在run里面把假事件池里面的方法拿出来一个个执行,自己执行(不是内置事件池去执行,是假事件池执行)解决了顺序和this问题还处理了事件对象的兼容性问题。on是往假事件池里面存方法,不是行为触发

13、柯理化函数思想之bind方法的兼容处理

参照12中的js代码

2-柯理化函数思想.html




    "UTF-8">
    珠峰培训
    "stylesheet" href="css/reset.min.css">
    






复制代码

2-1.js

var obj = {
    tagName: 'OBJ'
};

function fn() {
    console.log(this.tagName);
}

// document.body.onclick = fn;//=>this:body
// 想输出OBJ,需要让this变成obj  如下
document.body.onclick = fn.call(obj);//=>绑定方法的时候就把FN执行了(还没有点击页面就把fn执行了),把返回的UNDEFINED绑定给事件,点击时候什么都不处理

// -------------------------------------------------------------------------------------------------------

var obj = {
    tagName: 'OBJ'
};

function fn(e) {
    console.log(e,this.tagName);
}

// document.body.onclick = function (e) {
//     fn.call(obj, e);
// }; //=>这种方式可以  但是每次还需要包一层,包一层会形成一层作用域,以后作用域的查找、上级作用域链会多一层,不太好,所以用bind来解决

document.body.onclick = fn.bind(obj);//=>BIND:不仅把THIS预先处理为OBJ了,对于FN原本拥有的一些参数(例如:E)也没有忽略掉,执行的时候也传递给FN了;而且我们还可以自己传递一些参数;


// ------------------------------------------------------------------------------------------
// 自己传递参数

var obj = {
    tagName: 'OBJ'
};

function fn() {
    console.log(this.tagName,arguments);
}

document.body.onclick = fn.bind(obj, 100, 200);//=>obj不是参数是改变this执行的,这里100、200是参数,如果自己传递参数就不会有e这个值,由于所有参数都在arguments里面,所以我们输出arguments。 自己传递的参数不会覆盖默认的参数(先把自己传递的传递给FN,FN中最后一项参数才是默认的E)这里100和200不会覆盖掉默认参数e

// bind不仅可以预先改变this,还可以预先传递参数,并且默认参数也没有忽略掉(只不过放在所有参数的最末尾),但是bind不兼容

// --------------------------------------------------------------------------------------

// bind兼容写法如下  bind的方法在function的原型上,所以我们在function内置的原型上扩展一个方法myBind




// 首先分析内置bind()可以传递几个参数:再来封装一个兼容内置bind的方法myBind
var obj = {
    tagName: 'OBJ'
};

function fn() {
    console.log(this, arguments);
}


// document.body.onclick = fn.bind();
// // 执行myBind一个参数不传递时,this是window,严格模式下是undefiend,一个参数都不传递也行

// document.body.onclick = fn.bind(obj);
// // 但是传递一个参数obj时是把this执行obj,

// 但是不管一个参数都不传递还是只传递一个参数时,默认参数永远都不会丢失

// 传递两个参数时,arguments里面除了e这个默认参数以外,会多出一个参数100,默认参数e永远在末尾位置
document.body.onclick = fn.bind(obj,100);

// bind里面传递多少个参数不固定,但是第一个参数永远是需要预先改变的this值,如果不传递默认值是WINDOW
// 所以在Function的原型上扩展一个myBind方法时,myBind里面传递多少个参数也不固定,但是第一个参数永远是需要预先改变的this值,如果不传递默认值是WINDOW

//-----------------------------------------------------------------------------------------------------
// 封装的myBind

var obj = {
    tagName: 'OBJ'
};

function fn() {
    console.log(this, arguments);
}


Function.prototype.myBind = function myBind(context) {//在Function的原型上扩展一个myBind的方法
    //=>this:fn(当前我们需要预先处理THIS的函数)
    //=>context:我们需要预先改变的THIS值(如果不传递默认值是WINDOW)
    //=>arguments:存储包含CONTEXT在内的所有的实参(只能接收自己在执行MT-BIND时候传递的参数不包含默认参数,
    // 但是内置bind执行fn时arguments能得到默认参数e,并且默认参数e永远在末尾位置,所以我们)
    context = context || window;
    var outerArg = Array.prototype.slice.call(arguments, 1);//=>除了CONTEXT之外传递进来的参数值  借用数组原型上的slice方法将类数组装换成数组,让方法中的this变成arguments,
    var _this = this;
    // 由于这么写document.body.onclick = fn.myBind()并不是立马把fn执行,而是点击的时候才执行fn,所以点击的时候做一件什么事情的话,点击的时候要执行一个方法,所以
    // 执行myBind需要返回一个方法才能让点击document.body.onclick的时候做一些处理操作 => 所以在myBind里面返回一个匿名函数,fn.myBind()执行完返回的结果就是这个匿名函数,
   
    // return function () {//=>AAAFFF111 返回的匿名函数方法中的this是不固定的,但是外层方法中的this是fn,
    //     //=>_this:fn 
    //     _this.apply(context, outerArg);//执行fn并且让this变成context,把传递进来的参数值传递给fn方法  outerArg是一个数组(存了所有参数)所以这里用apply不用call 但是outerArg里面没有e,
    //     //那么e在哪里呢?=> 这里需要注意的是:我们执行myBind是这个匿名函数返回给document.body.onclick,所以e不在myBind方法里面,在这个匿名函数里面,所以如下
    // }

    // // 由于e不在myBind方法里面,在这个匿名函数里面,需要把e放到这个outerArg并且再末尾位置
    // return function (e) {
    //     outerArg.push(e);//点击里面默认参数传递的是e,但是别的方法操作传的默认参数可能不是事件对象e也得传给outerArg,所以如下,
    //     _this.apply(context, outerArg);
    // }

    // 由于别的方法操作传的默认参数可能不仅是事件对象e也得传给outerArg
    return function () {
        //=>_this:fn
        //=>arguments:返回的匿名函数接收到的参数
       var innerArg = Array.prototype.slice.call(arguments);//把当前匿名函数中接收到的所有参数原封不动赋值给innerArg这个数组
       outerArg = outerArg.concat(innerArg);//把这两个数组合并,并且innerArg在末尾
        _this.apply(context, outerArg);
    }

};
// fn.myBind(obj, 100, 200);//这样写没fn有默认参数e  这样执行myBind方法中的this是fn
document.body.onclick = fn.myBind(obj, 100, 200);//这样fn有默认参数e,浏览器触发click事件的时候,浏览器执行fn给fn会传递参数e(事件对象)。  这样执行myBind方法中的this也是fn


// 分析
// document.body.onclick = fn.myBind(obj, 100, 200) 
// 等价于:
// docu ment.body.onclick = function (e) {//=>AAAFFF111 
    
// }
// 所以当点击document.body.onclick的时候执行fn.myBind并没有把函数fn执行, 但是点击的时候要执行fn,怎么办呢? 
// 所以我们执行fn.myBind()的时候先返回一个匿名函数,当点击的时候先执行这个匿名函数(地址AAAFFF111),在匿名函数中执行fn
// (我们最终目的要执行fn,并且让fn里面的this变成obj,并且把参数值和默认参数值都传递给fn)
复制代码

myBind兼容处理的原理:

首先第一步执行myBind这个方法,当执行myBind这个方法的时候形成一个私有作用域,在私有作用域当中返回一个函数,把返回的函数绑定给document.body.onclick这个事件,当触发document.body.onclick点击行为的时候执行的是返回的这个函数,

所以当前形成的这个私有作用域不销毁。(执行一个函数返回一个引用数据类型值,并且这个地址被外面占用了,当前作用域不销毁)当前这个私有作用域不销毁,所以context和outerArg 都存起来了,每次执行myBind都形成一个不销毁的私有作用域,而且这个私有作用域里面把传递进来的参数outerArg和context都存起来了,这个就叫做预先处理,返回一个匿名函数,把匿名函数绑定给document.body.onclick ,当点击的时候先执行这个匿名函数,执行匿名函数的时候需要用到之前存的参数outerArg和context,直接拿过来用就可以了。这种思想就叫柯理化函数思想,它的本意就是预先处理的思想

那什么叫预先处理思想呢?就是形成一个不销毁的闭包把以后要用到的值先存起来,以后用的时候直接拿过来用就可以了,这就叫柯理化函数思想,也就是对应了闭包的第二个作用=>保存

惰性思想也用了闭包的保存和保护,(保存用的比较多,也是先把一些后期需要用到的值先存起来,以后要用的时候直接拿过来用就可以了),柯理化函数思想也是用了闭包的保存机制,把一些后期要用到的值先保存起来,以后用的时候直接拿过来用,但是柯理化函数思想能实现预处理的操作,比如说

fn.myBind()执行myBind已经预先把this(obj)和参数(100、200)这些都处理好了,当点击docu ment.body.onclick执行fn.myBind()这个方法的时候先执行返回的匿名函数,然后在返回的函数中执行fn,返回的匿名函数中的this已经变成预先处理的值(_this),这种利用闭包的保存机制集合到真实项目当中预先处理的思想就叫柯理化函数思想

柯理化函数思想还是闭包的保存作用,之前写的惰性思想还是闭包的保存和保护作用。

惰性思想和柯理化函数思想的核心原理就是闭包,而闭包的核心原理都在堆栈内存的释放和不释放问题(销毁和不销毁问题),而堆栈内存的销毁和不销毁都在于数据类型这块知识点,数据类型知识点就是整个js最低层原理。

所谓的高级编程思想基于 ——> 闭包的销毁和不销毁基于 ——> 堆栈内存 ——> 数据类型

再写一遍myBin兼容处理

Function.prototype.myBind = function myBind(context) {
    context = context || window;
    var _this = this,
        outerArg = Array.prototype.slice.call(arguments,1);
    return function () {
        var innerArg = Array.prototype.slice.call(arguments);
            outerArg = outerArg.concat(innerArg);
            _this.apply(context,outerArg);
    }
}

复制代码

event-back1.js可以改写成event-back2.js(自己封装的bind方法的兼容处理)

Function.prototype.myBind = function myBind(context) {
    context = context || window;
    var _this = this,
        outerArg = Array.prototype.slice.call(arguments, 1);
    if ('bind' in Function.prototype) {//标准浏览器
        outerArg.unshift(context);
        return _this.bind.apply(_this, outerArg);//执行bind虽然传递的参数outerArg是个数组,但是也会一个个的给bind传递参数,所以需要apply
    }
    return function () {
        var innerArg = Array.prototype.slice.call(arguments);
        outerArg = outerArg.concat(innerArg);
        _this.apply(context, outerArg);
    }
};

//=>ON:向事件池中追加方法
function on(curEle, type, fn) {
    if (document.addEventListener) {
        curEle.addEventListener(type, fn, false);
        return;
    }
    if (typeof curEle['pond' + type] === 'undefined') {
        curEle['pond' + type] = [];

        

        // curEle.attachEvent('on' + type, run.bind(curEle));

        curEle.attachEvent('on' + type, run.myBind(curEle));//这里就用封装的myBind。兼容ie6~8
        // 等价于
        // curEle.attachEvent('on' + type, function (e) {
        //     run.apply(curEle, [e]);
        // });


    }
    var pondAry = curEle['pond' + type];
    for (var i = 0; i < pondAry.length; i++) {
        if (pondAry[i] === fn) {
            return;
        }
    }
    pondAry.push(fn);
}
//=>OFF:移除事件池中的某个方法
function off(curEle, type, fn) {
    if (document.removeEventListener) {
        curEle.removeEventListener(type, fn, false);
        return;
    }
    var pondAry = curEle['pond' + type];
    if (!pondAry) return;
    for (var i = 0; i < pondAry.length; i++) {
        if (pondAry[i] === fn) {
            pondAry[i] = null;
            break;
        }
    }
}
//=>RUN:把自己造的假事件池中的方法依次执行
function run(e) {
    if (typeof e.target === 'undefined') {
        e.target = e.srcElement;
        e.which = e.keyCode;
        e.pageX = e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft);
        e.pageY = e.clientY + (document.documentElement.scrollTop || document.body.scrollTop);
        e.preventDefault = function () {
            e.returnValue = false;
        };
        e.stopPropagation = function () {
            e.cancelBubble = true;
        };
    }
    var pondAry = this['pond' + e.type];
    if (!pondAry) return;
    for (var i = 0; i < pondAry.length; i++) {
        var itemFn = pondAry[i];
        if (itemFn === null) {
            pondAry.splice(i, 1);
            i--;
            continue;
        }
        itemFn.call(this, e);
    }
}
复制代码

14、DOM2兼容处理(使用ES6封装)

1-event.html




    "UTF-8">
    
    "stylesheet" href="css/reset.min.css">
    







复制代码

1-1.js

var fn1 = function (e) {
    console.log(1);
};
var fn2 = function (e) {
    console.log(2);
};
var fn3 = function (e) {
    console.log(3);
};
var fn4 = function (e) {
    console.log(4);
    $event.off(document.body, 'click', fn1);//通过$event区绑定on或者off方法
    $event.off(document.body, 'click', fn2);
    $event.off(document.body, 'click', fn3);
};
var fn5 = function (e) {
    console.log(5);
};
var fn6 = function (e) {
    console.log(6);
};
var fn7 = function (e) {
    console.log(7);
};
var fn8 = function (e) {
    console.log(8);
};
var fn9 = function (e) {
    console.log(9);
};
var fn10 = function (e) {
    console.log(10);
};
var fn11 = function (e) {
    console.log(11);
};
var fn12 = function (e) {
    console.log(12);
};

$event.on(document.body, 'click', fn1);
$event.on(document.body, 'click', fn2);
$event.on(document.body, 'click', fn3);
$event.on(document.body, 'click', fn4);
$event.on(document.body, 'click', fn5);
$event.on(document.body, 'click', fn6);
$event.on(document.body, 'click', fn7);
$event.on(document.body, 'click', fn8);
$event.on(document.body, 'click', fn9);
$event.on(document.body, 'click', fn10);
$event.on(document.body, 'click', fn11);
$event.on(document.body, 'click', fn12);
$event.on(document.body, 'click', fn12);
$event.on(document.body, 'click', fn12);
$event.on(document.body, 'click', fn12);
复制代码

event.js

~function () {//自执行函数形成一个私有作用域 
    Function.prototype.myBind = function myBind(context = window, ...outer) {//es6语法封装myBind  剩余运算符获取所有outer参数是个数组
        // if(Function.prototype.bind){ return this.bind(...arguments);}
        if ('bind' in this) {//如果有这个bind这个方法  in不管私有公有上只要有bind这个方法
            return this.bind(...arguments);
        }
        return (...inner) => this.apply(context, outer.concat(inner));//箭头函数中的this就是当前宿主环境中的this(当前需要处理的函数myBind),APPLY改变this指向,让this指向context
    };

    let on = function (curEle, type, fn) {
        if (document.addEventListener) {//标准浏览器
            curEle.addEventListener(type, fn, false);
            return;
        }
        // ie6~8不兼容创建事件池处理
        if (typeof curEle['pond' + type] === 'undefined') {
            curEle['pond' + type] = [];
            // curEle.attachEvent('on' + type, run);//这样执行run中的this不是curEle,是window
            // curEle.attachEvent('on' + type, function (e) {
            //     run.call(curEle,e);
            // });//这样写可以
            curEle.attachEvent('on' + type, run.myBind(curEle));//on中把run方法放到内置事件池中 封装myBind改变run中的this指向(让this指向curEle)
        }
        let pondAry = curEle['pond' + type];
        for (let i = 0; i < pondAry.length; i++) {//去重
            if (pondAry[i] === fn) {
                return;
            }
        }
        pondAry.push(fn);
    };

    let off = function (curEle, type, fn) {
        if (document.removeEventListener) {//标准浏览器
            curEle.removeEventListener(type, fn, false);
            return;
        }
        // ie6~8不兼容处理
        let pondAry = curEle['pond' + type];
        if (!pondAry) return;
        for (let i = 0; i < pondAry.length; i++) {
            let itemFn = pondAry[i];
            if (itemFn === fn) {
                pondAry[i] = null;//不能删除掉只能赋值成null,不然会引起数组塌陷
                break;
            }
        }
    };

    // run只会在ie6~8下才会执行
    let run = function (e) {
        e = e || window.event;
        if (!e.target) {//处理事件对象兼容
            e.target = e.srcElement;
            e.pageX = e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft);
            e.pageY = e.clientY + (document.documentElement.scrollTop || document.body.scrollTop);
            e.which = e.keyCode;
            e.preventDefault = function () {
                e.returnValue = false;
            };
            e.stopPropagation = function () {
                e.cancelBubble = true;
            };
        }
        let pondAry = this['pond' + e.type];
        if (!pondAry) return;
        for (let i = 0; i < pondAry.length; i++) {
            let itemFn = pondAry[i];
            if (itemFn === null) {
                pondAry.splice(i, 1);
                i--;
                continue;
            }
            itemFn.call(this, e);
        }
    };

    // 这三个on,off,run方法都在私有当中(run在on里面所以不需要暴露),暴露着三个方法,myBind不用暴露因为在原型上可以直接用
    window.$event = {
        on: on,
        off: off
    };
}();


// 以后通过$event使用暴露出来的方法
// $event.on(document.body, 'click', fn1);
// $event.off(document.body, 'click', fn1);
复制代码

编译成es5

安装编译模块

新建.babelrc,添加配置

安装成功后会有node_modules文件夹

把event.js编译成eventTool.js

标准浏览器下点击第一次

标准浏览器下点击第二次

ie浏览器(高版本ie和低版本ie)下点击第一次

ie浏览器(高版本ie和低版本ie)下点击第二次

15、使用封装的事件库实现拖拽

WEEK6-DAY3-DRAG-index.html




    "UTF-8">
    

    
    "stylesheet" href="css/reset.min.css">
    



"box" id="box">
复制代码

WEEK6-DAY3-DRAG- index-back.js

let down = function (e) {
    //=>this:oBox
    this.strX = e.clientX;
    this.strY = e.clientY;
    this.strL = this.offsetLeft;
    this.strT = this.offsetTop;

    // $event.on(document, 'mousemove', move);//这样写move中的this是document  通过myBind改变this指向(指向oBox)
    // $event.on(document, 'mouseup', up);

    //=>绑定方法的时候,向事件池中存放的是执行MY-BIND返回的匿名函数(问题:移除的时候不知道移除谁)
    // $event.on(document, 'mousemove', move.myBind(this));
    // $event.on(document, 'mouseup', up.myBind(this));

    this._MOVE = move.myBind(this);
    this._UP = up.myBind(this);
    $event.on(document, 'mousemove', this._MOVE);
    $event.on(document, 'mouseup', this._UP);
};

let move = function (e) {
    //=>this:oBox
    let curL = e.clientX - this.strX + this.strL,
        curT = e.clientY - this.strY + this.strT;
    curL = curL < 0 ? 0 : (curL > maxL ? maxL : curL);
    curT = curT < 0 ? 0 : (curT > maxT ? maxT : curT);
    this.style.left = curL + 'px';
    this.style.top = curT + 'px';
};

let up = function (e) {
    //=>this:oBox
    console.log(111);
    $event.off(document, 'mousemove', this._MOVE);
    $event.off(document, 'mouseup', this._UP);
};

let oBox = document.getElementById('box'),
    maxL = (document.documentElement.clientWidth || document.body.clientWidth) - oBox.offsetWidth,
    maxT = (document.documentElement.clientHeight || document.body.clientHeight) - oBox.offsetHeight;

$event.on(oBox, 'mousedown', down);
复制代码

16、弹性势能动画-水平运动

水平方向运动的速度取决于拖拽即将结束松开手的一瞬间的速度

那怎么来获取拖拽即将结束松开手一瞬间的速度呢?

同样的距离,速度越快,所用的时间越少,反应的次数越少,MouseMove就被触发的就越少

同样的距离,速度越慢,所用的时间越长,反应的次数越多,MouseMove就被触发的就越多

假如浏览器每10ms反应一次触发一次move(每隔10ms执行一次move),移动的距离越长说明时间越少,反应的次数越少,MouseMove就被触发的就越少,反之亦然。

水平方向运动的速度取决于最后即将松开手瞬间的移动速度

WEEK6-DAY3-DRAG-index.html




    "UTF-8">
    
    "stylesheet" href="css/reset.min.css">
    



"box" id="box">
复制代码

WEEK6-DAY3-DRAG-index-backFly.js

let down = function (e) {
    this.strX = e.clientX;
    this.strY = e.clientY;
    this.strL = this.offsetLeft;
    this.strT = this.offsetTop;

    this._MOVE = move.myBind(this);
    this._UP = up.myBind(this);
    $event.on(document, 'mousemove', this._MOVE);
    $event.on(document, 'mouseup', this._UP);
};

let move = function (e) {
    let curL = e.clientX - this.strX + this.strL,
        curT = e.clientY - this.strY + this.strT;
    curL = curL < 0 ? 0 : (curL > maxL ? maxL : curL);
    curT = curT < 0 ? 0 : (curT > maxT ? maxT : curT);
    this.style.left = curL + 'px';
    this.style.top = curT + 'px';

    //=>计算水平方向的速度  当前位置减去元素上一次位置值就是当前移动的距离(用一个自定义属性保存这个计算值) 如下计算
    if (!this.preFly) {//this.preFly:上一次位置值  由于第一次进来的时候没有上一次位置值,所以把当前位置值赋值给this.preFly
        this.preFly = this.offsetLeft;
    } else {
        this.speedFly = this.offsetLeft - this.preFly;//每一次移动的距离(用自定义属性this.speedFly保存),假如浏览器每10ms反应一次触发一次move(每隔10ms执行一次move),移动的距离越长说明时间越少,反应的次数越少,MouseMove就被触发的就越少,反之亦然
        this.preFly = this.offsetLeft;//当前值this.offsetLeft作为下一次的上一次位置值
    }
};

let up = function (e) {
    $event.off(document, 'mousemove', this._MOVE);
    $event.off(document, 'mouseup', this._UP);

    //=>松开鼠标后,让盒子飞吧
    moveFly.call(this);//=>让this指向oBox
};

//=>水平方向飞
let moveFly = function () {
    //=>this:oBox
    let speedFly = this.speedFly;//获取到水平方向速度
    this.timerFly = setInterval(()=> {
        //=>由于JS盒子模型属性获取的结果是整数(会四舍五入),所以速度如果小于0.5,我们本次加的速度值在下一次获取的时候还会被省略掉(此时盒子应该是不动的)
        if (Math.abs(speedFly) < 0.5) {
            clearInterval(this.timerFly);
            return;
        }
        //=>指数衰减的运动(速度乘以小于1的值肯定越来越小)
        speedFly *= 0.98;
        let curL = this.offsetLeft + speedFly;
        if (curL > maxL || curL < 0) {
            speedFly *= -1;//=>控制反方向运动
            curL = curL > maxL ? maxL : (curL < 0 ? 0 : curL);//=>控制不管怎么走都不要超过边界
        }
        this.style.left = curL + 'px';
    }, 17);
};


let oBox = document.getElementById('box'),
    maxL = (document.documentElement.clientWidth || document.body.clientWidth) - oBox.offsetWidth,
    maxT = (document.documentElement.clientHeight || document.body.clientHeight) - oBox.offsetHeight;

$event.on(oBox, 'mousedown', down);


/*
 * 事件触发
 *   1、浏览器都有一个自己最小的反应时间,当我们移动过程中,浏览器总会在最小的反应时间内触发MOUSE-MOVE事件行为
 *   移动相同距离之下
 *   =>移动速度快,浏览器反应过来的次数就少(触发事件的次数也就少)
 *   =>移动速度慢,浏览器反应过来的次数就多(触发事件的次数也就多)
 *
 *   2、水平方向运动的速度取决于最后即将松开手瞬间的移动速度
 */
复制代码

首先mousemove事件触发执行move方法,所以我们需要在move中计算出最后一次鼠标即将离开的时候移动的距离(最后一次移动的距离才决定水平方向上的速度,跟之前移动的距离没关系)

刚开始进来没有上一次的位置值,把this.offsetLeft当前位置赋值给this.preFly作为第一次进来的位置值(this.preFly:保存上一次位置值),

第二次进来就有了上一次的位置值,用当前位置this.offsetLeft减去上一次位置值this.preFly,就是当前距离(速度)

第二次不应当是最后一次,如果有第三次,需要把第二次的位置值赋值给this.preFly作为第三次位置值的上一次位置值

如果有第三次,用第三次的最新位置值this.offsetLeft减去存的第二次的位置值this.preFly就是本次的移动距离

同样第三次也不一定是最后一次,把第三次的位置值赋值给this.preFly作为第四次位置值的上一次位置值

以此类推...

到最后一次的时候,speedFly 得到的结果永远是最后一次的值

水平方向速度计算完后,松开鼠标让盒子水平方向飞:moveFly.call(this);//=>让this指向oBox

17、弹性势能动画-垂直运动

WEEK6-DAY3-DRAG-index.html




    "UTF-8">
    
    "stylesheet" href="css/reset.min.css">
    


"box" id="box">"hudie.png" alt="">
"box" id="box2">
复制代码

WEEK6-DAY3-DRAG-index.js

let imgList = document.getElementsByTagName('img');
for (let i = 0; i < imgList.length; i++) {
    $event.on(imgList[i], 'mousemove', function (e) {
        e.preventDefault();
    });
}//阻止图片的默认行为 如果页面当中是摁住图片在拉的时候,是相当于把当前图片的虚影拉倒一个位置,鼠标会丢失原图片

let down = function (e) {
    this.strX = e.clientX;
    this.strY = e.clientY;
    this.strL = this.offsetLeft;
    this.strT = this.offsetTop;

    this._MOVE = move.myBind(this);
    this._UP = up.myBind(this);
    $event.on(document, 'mousemove', this._MOVE);
    $event.on(document, 'mouseup', this._UP);

    //=>结束当前盒子正在运行的动画,开始新的拖拽
    clearInterval(this.timerDrop);
    clearInterval(this.timerFly);
};

let move = function (e) {
    let curL = e.clientX - this.strX + this.strL,
        curT = e.clientY - this.strY + this.strT;
    curL = curL < 0 ? 0 : (curL > maxL ? maxL : curL);
    curT = curT < 0 ? 0 : (curT > maxT ? maxT : curT);
    this.style.left = curL + 'px';
    this.style.top = curT + 'px';

    //=>计算水平方向的速度
    if (!this.preFly) {
        this.preFly = this.offsetLeft;
    } else {
        this.speedFly = this.offsetLeft - this.preFly;
        this.preFly = this.offsetLeft;
    }
};

let up = function (e) {
    $event.off(document, 'mousemove', this._MOVE);
    $event.off(document, 'mouseup', this._UP);

    //=>松开鼠标后,让盒子飞吧
    moveFly.call(this);
    moveDrop.call(this);
};

//=>水平方向飞
let moveFly = function () {
    let speedFly = this.speedFly;
    this.timerFly = setInterval(()=> {
        if (Math.abs(speedFly) < 0.5) {
            clearInterval(this.timerFly);
            return;
        }
        speedFly *= 0.98;
        let curL = this.offsetLeft + speedFly;
        if (curL > maxL || curL < 0) {
            speedFly *= -1;
            curL = curL > maxL ? maxL : (curL < 0 ? 0 : curL);
        }
        this.style.left = curL + 'px';
    }, 17);
};

//=>垂直方向飞
//垂直方向也有个速度,这个速度(speedDrop)是固定的,跟高度没关系,跟盒子大小没关系,按着这个速度上下飞就行了
// 垂直方向跟拖拽没关系,跟
let moveDrop = function () {
    let speedDrop = 10,
        flagDrop = 0;
    this.timerDrop = setInterval(()=> {
        if (flagDrop >= 2) {//盒子掉到底边弹不起来了(就一直在底边了的时候)flagDrop++ 将会大于等于2
            clearInterval(this.timerDrop);
            return;
        }
        speedDrop *= 0.98;
        speedDrop += 10;
        let curT = this.offsetTop + speedDrop;
        if (curT >= maxT) {
            speedDrop *= -1;
            curT = maxT;
            flagDrop++;//盒子掉到底边让flagDrop++
        } else {
            flagDrop = 0;//盒子掉到底边弹起来flagDrop = 0
        }
        this.style.top = curT + 'px';
    }, 17);
};


let oBox = document.getElementById('box'),
    oBox2 = document.getElementById('box2'),
    maxL = (document.documentElement.clientWidth || document.body.clientWidth) - oBox.offsetWidth,
    maxT = (document.documentElement.clientHeight || document.body.clientHeight) - oBox.offsetHeight;

$event.on(oBox, 'mousedown', down);
$event.on(oBox2, 'mousedown', down);
复制代码

你可能感兴趣的:(javascript)