DOM0事件和DOM2事件模型 —— JS中的设计模式和组件封装

1,733 阅读19分钟

1、一些对于技术发展的心得体会

学习各种设计模式有什么作用?

【开发】

开发效率高

利于团队协作

【维护】

有利于代码的升级改变

逻辑清晰,代码严谨,利于后期的维护

【通用】

我们依托设计模式可以实现组件化、模块化、插件化、框架化以及一些常用类库方法的编写

技术语言发展路线

语言语法更新迭代之路(路漫漫而其修远兮)

语法初步稳定阶段 -> 研究核心语法和使用的早一批人 ->封装各种类库和插件 ->大量研究核心的开发人员… ->出现各种设计模式 ->框架(VUE、REACT)

2、类库、组件、插件、框架四者的区别

类库:提供一些真实项目开发中常用的方法(方法做了完善处理:兼容处理、细节优化),方便我们开发和维护 [ jQuery、Zepto… ]

插件:把项目中某一部分进行插件封装(是具备具体的业务逻辑的,更加有针对性),以后再有类似的需求,直接导入插件即可,相关业务逻辑代码不需要自己在编写了 [ jquery.drag.js 、jquery.dialog.js、jquery.validate.min.js 、datepicker日历插件、echarts统计图插件、iscroll插件…]

组件:类似于插件,但是插件一般只是把JS部分封装,组件不仅封装了JS部分,而且把CSS部分也封装了,以后再使用的时候,我们直接的按照文档使用说明引入CSS/JS,搭建对应的结构,什么都不用做功能自然就有了 [ swiper组件、bootstrap组件… ]

框架:比上面的三个都要庞大,它不仅仅提供了很多常用的方法、而且也可以支持一些插件的扩展(可以把一些插件集成到框架中运行)、更重要的是提供了非常优秀的代码管理设计思想… [ REACT、VUE、ANGULAR、REACT NATIVE… ]

3、JS中常用的设计模式

单例设计模式、构造原型设计模式、发布订阅设计模式、promise设计模式…

单例设计模式

1.//=>单例模式:把实现当前这个模块所有的属性和方法汇总到同一个命名空间下(分组作用,避免了全局变量的污染)
2.let exampleRender=(function(){//js惰性思想 通过闭包先形成一个不销毁的私有作用域
3.    //=>实现当前模块具体业务逻辑的方法全部存放在闭包中
4.    let fn=function(){
5.        //...
6.    }
7.
8.    return {
9.        init:function(){
10.            //=>入口方法:控制当前模块具体的业务逻辑顺序
11.            fn();
12.        }
13.    }
14.})();
15.exampleRender.init();

真实项目中,我们如果想要实现具体的业务逻辑需求,都可以依托于单例模式构建;我们把项目划分成各大板块或者模块(模块化开发),把实现同一个板块的方法放在一个独立的命名空间下,方便团队协作开发;

构造原型模式:最贴近OOP面向对象编程思想的

以后真实项目中,不管是封装类库还是插件或者UI组件(既有样式和结构),基本上都是基于构造原型模式来开发的

1.class Tool{ //创建一个Tool类(常用的工具方法库) 存放公共方法
2.    constructor(){ //构造函数  所有私有东西放到构造函数中  这里的this是当前Tool这个方法的实例
3.        this.isCompatible='addEventListener' in document;//=>如果不兼容返回FALSE(IE6~8)
        //验证ie6~8兼容:通过getComputedStyle、getelementbyclassname、addEventListener都可以判断
4.          
5.    }
6.    //=>挂载到原型上的方法 实例可以调用的方法挂载到原型上
7.    css(){
8.        //...
9.    }
10.    //=>挂载到普通对象上的方法
11.    static distinct(){
12.        //...
13.    }
14.}
15.
16.class Banner extends Tool{ //Banner继承Tool 可以使用Tool
17.    constructor(...arg){ //...arg:当前Banner这个类接收到的所有参数
18.        super(...arg);//es6继承 必须写super  ...arg:把当前Banner接收到的参数传给super
19.        this.xxx=xxx;//给当前Banner添加私有属性
20.    }
21.    //=>挂载到子类原型上的方法
22.    bindData(){//这个方法中的this是子类的实例,子类的实例不仅可以调取子类的私有属性和子类上的方法,还可以调取它继承父类上的原型上的方法,但是父类做为普通对象加入的静态方法不能继承
23.        this.css();//=>把父类原型上的方法执行(子类继承了父类,那么子类的实例就可以调取父类原型上的方法了)
24.        this.distinct===undefined;//=>子类的实例只能调取父类原型上的方法,以及父类给实例提供的私有属性方法,但是父类做为普通对象加入的静态方法,子类的实例是无法调取的 (只有这样才可以调取使用:Tool.distinct())
25.    }
26.}

// new Banner()

插件里面一般会有很多个类,比如有5个类,其中4个类都是工具类(提供的都是一些常用的方法),只有最后一个类才是实现具体业务逻辑的,所以最后一个类需要继承前面所有的类,那一个类怎么继承多个类呢?如下:

我有三个类 A/B/C ,我想让C继承A和B

//一次继承只能继承一个类,没有办法批量继承  但是真正继承还需要考虑参数的传递  这里只是写个大体结构
1.class A{
2.    ...
3.}
4.class B extends A{ //B继承A后,B里面可以用A里面提供的私有属性和原型上的方法
5.    ...
6.}
7.class C extends B{ //C继承B后,C里面可以用B里面提供的私有属性和原型上的方法  C能用B,B能用A,C能用B和A
8.    ...
9.}

发布订阅设计模式:观察者模式

不同于单例和构造,发布订阅是小型设计模式,应用到某一个具体的需求中:凡是当到达某个条件之后要执行N多方法,我们都可以依托于发布订阅设计模式管理和规划我们的JS代码

我们经常把发布订阅设计模式嵌套到其它的设计模式中

promise设计模式

同步可以层级嵌套(先完成A再完成B),但是异步不能层级嵌套(因为还没执行完A就会执行B)

解决AJAX异步请求层级嵌套的问题

它也是小型设计模式,目的是为了解决层级嵌套问题的,我们也会经常把它嵌套在其它的设计模式中运行

1.$.ajax({
2.    url:'/A',
3.    async:true,//=>异步
4.    success:function(result){
5.        $.ajax({
6.            url:'/B',
7.            async:true,
8.            success:function(){
9.                //=>还会有后续嵌套  以后嵌套多了会很乱也不好管理
10.            }
11.        });     
12.    }
13.});

项目中,掌握了设计模式会发现实现某一个模块或者某个区域的功能或者方法,最终使用n多个设计模式共同组合开发的(可能会单列,发布订阅,promise结合一起完成一个功能)

常用的设计模式基本上就是以上四个,还有更多设计模式

4、发布订阅设计模式的核心思想

俗称叫做“观察者模式”

实现思路和原理:

1、我们先创建一个计划表(容器)

2、后期需要做什么事情,我们都依次把需要处理的事情增加到计划表中

3、当符合某个条件的时候,我们只需要通知计划表中的方法按照顺序依次执行即可

1-发布订阅.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<!-- <script src="js/animate.min.js"></script> -->
<script src="js/animate.js"></script>
<script>

    
    let fn1 = function () {
        console.log(1);
    };
    
    let fn2 = function () {
        console.log(2);
    };
    
    let fn3 = function () {
        console.log(3);
    };

    animate({
        curEle: document.body,
        target: {
            opacity: 0.2
        },
        duration: 1000,
        callBack:fn1
    });

    // --------以上动画执行完只能执行fn1,现在想动画执行结束后三个方法都能执行-------
    // --------动画执行结束后三个方法都能执行-------------------------------------

   let fn1 = function () {
        console.log(1);
    };
    
    let fn2 = function () {
        console.log(2);
    };
    
    let fn3 = function () {
        console.log(3);
    };

    animate({
        curEle: document.body,
        target: {
            opacity: 0.2
        },
        duration: 1000,
        callBack:function(){
            fn1();
            fn2();
            fn3();
        }
    });


    // 但是这样写不方便后期维护,回调函数多嵌套了一层函数,并且如果回调函数里需要执行的函数不
    // 仅仅只有三个时,就不方便维护(这样写必须要把当前动画结束完成后需要执行的方法都写出来),
    // 所以需要用发布订阅来解决,

    // 发布订阅的实现思路和原理:
    // 1、我们先创建一个计划表(容器) 
    // 2、后期需要做什么事情,我们都依次把需要处理的事情增加到计划表中 
    // 3、当符合某个条件的时候,我们只需要通知计划表中的方法按照顺序依次执行即可

    // 发布订阅跟dom2事件池兼容思想类似:
    // 1、先创建一个假事件池,
    // 2、所有点击时候需要做的事情通过on先一个个放到事件池中但是并没有执行,当要点击时才去执行,但是
    // 点击时到底需要执行多少个方法并不知道,但是没关系,想要执行多少个方法都通过on放到假事件池中去,
    // 3、当点击时通过run方法让run方法把事件池中的方法依次执行


    //-----用发布订阅思想来实现以上代码-------------------------------------


    let plan = [];//容器计划表

    let fn1 = function () {
        console.log(1);
    };
    plan.push(fn1);//要执行多少个方法并不知道,需要执行一个方法就push到容器中


    let fn2 = function () {
        console.log(2);
    };
    plan.push(fn2);


    animate({
        curEle: document.body,
        target: {
            opacity: 0.2
        },
        duration: 1000
    });

    //.....执行很多很多代码......
    let fn3 = function () {
        console.log(3);
    };
    plan.push(fn3);

    // 然后animate.js中需要把callBack && callBack.call(curEle)这一步改成循环数组plan,把plan中每个方法依次执行

</script>
</body>
</html>

5、JQ中的发布订阅模式

发布订阅如果你会的话,在整个项目当中代码管理和设计管理上都非常方便

比如之前的动画,动画完成之后要做的哪些事情可以使用发布订阅

比如拖拽,拖拽按下的时候想额外做一些事情,移动的时候想额外做一些事情,松开的时候想额外做一些事情,也可以通过发布订阅创建容器计划表来实现

还有选项卡,按下的时候不仅要实现切换,还想要实现一些其他的操作,也可以通过发布订阅来管理

凡是在某些条件下不仅要执行一个方法,还想执行其他很多方法,都可以用发布订阅来做,很常用

JQ中的发布订阅

JQ中提供了实现发布订阅设计模式的方法

1.let $plan = $.Callbacks();//=>创建一个计划表
//Callbacks回调函数集合,发布订阅其实就是一堆回调函数集合(创建一个计划表,计划表里面所有要做的事情都是当某个条件
//成功触发之后要做的事情(其实就是回调函数))
2.
3.let fn = function(n,m){
4.    //=>n=100 m=200
5.}
6.$plan.add(fn);//=>向计划表中增加方法
7.$plan.remove(fn);//=>从计划表中移除方法
8.
9.$plan.fire(100,200);//=>通知计划表中所有的方法按照顺序执行;100 200会分别作为实参传递给每一个需要执行的方法;

举个列子

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<script src="js/jquery-1.11.3.min.js"></script>
<script>
    let $plan = $.Callbacks();
    $plan.add(function () {
        console.log(1);
    });

    setTimeout($plan.fire, 1000);//一秒钟之后执行fire方法,通过fire方法会把$plan
    // 里面左右存的方法都依次执行,后期到底还会存什么方法不用管了,直接add就是了


    $plan.add(function () {
        console.log(2);
    });
</script>
</body>
</html>

6、封装属于自己的发布订阅模式库

封装一个类似于jq的发布订阅模式库

1、基于构造函数封装

2、模拟JQ的操作步骤

3、注意数组塌陷问题

4、封装EACH遍历数组中的每一项

WEEK6-DAY4-js-callbacks-3-CALLBACKS.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<script src="js/callbacks-backup.js"></script>
</body>
</html>

WEEK6-DAY4-js-callbacks-backup.js

~function () {
    //=>EACH:遍历数组中每一项的内容 ,for each不兼容,封装个each方法(项目中一般都会自己封装一个each循环)
    let each = function (ary, callBack) {
        for (let i = 0; i < ary.length; i++) {
            let result = callBack && callBack(ary[i], i);//执行回调函数并且接收回调函数的返回结果

            //=>如果回调函数中返回FALSE,代表结束当前正在遍历的操作(仿照JQ中的EACH语法实现的)
            if (result === false) break;

            //=>如果回调函数中返回的是DEL,代表当前这一项在回调函数中被删除了,为了防止数组塌陷问题,我们让索引减减(如图:each方法的一点问题)
            if (result === 'DEL') i--;
        }
    };
    // each([12,23],function(item,index){//item:当前遍历对象 index:当前遍历对象的索引
    //     return false;//回调函数中返回false
    // });

    class Plan {
        constructor() {//Plan的构造函数
            this.planList = [];//=>存放方法的容器  通过this放在这个实例上
        }

        //=>挂载到PLAN原型上的方法
        add(fn) {
            let planList = this.planList,
                flag = true;
            //=>去重处理 已经增加过的方法不再增加  自己封装个each方法遍历
            each(planList, function (item, index) {

                // if(item === fn){
                //     return false;//这样写  这个return false只会结束当前的each循环这个方法,
                //     还是会继续往下执行,执行add这个方法中的push,所以用flag标识来做处理
                // }

                if (item === fn) flag = false;
                return flag;
            });
            flag ? planList.push(fn) : null;
        }

        remove(fn) {
            let planList = this.planList;
            each(planList, function (item, index) {
                if (item === fn) {
                    //planList.splice(index, 1);
                    //=>这样会引起数组塌陷(详情见图:数组塌陷)

                    planList[index] = null;
                    //=>这样处理位置存在(索引在),但是值没有了
                    return false;
                }
            });
        }

        fire(...arg) {//在fire中传递进来的实参,相当于让容器当中每个方法里面都能接收到这个参数值  剩余运算符得到这个值 把传给fire所有的参数全部写在arg里面,此时arg是个数组
            let planList = this.planList;
            each(planList, function (item, index) {
                if (item === null) {
                    //=>当前项是已经被REMOVE移除掉的 
                    planList.splice(index, 1); // 在回调函数中删除对each并没有影响,在each函数里的for循环中i会继续++,这时候就会跳过fn2(如图:each方法的一点问题)  
                    //  所以需要在each中去为了防止数组塌陷问题,我们让索引减减  
                    return 'DEL';// 只能通过返回值(返回一个标识)去做特殊处理 (each方法中的callbak接收到这个返回标识做特殊处理) 
                }
                item(...arg);//展开运算符一项项传给item并执行(item就是需要执行的方法)  等价于item.apply(null,[100,200,300]) (item里面不能传数组只能一项一项的传递) 
            });
        }

        //=>挂载到PLAN对象上的属性和方法
        static Callbacks() {
            return new Plan();//执行 Plan.Callbacks()返回Plan实例  实例才可以调取原型上的add、remove、fire方法,
            // 但是不仅要返回这个实例还需要创建一个计划表(容器:要放很多方法)
            // new Plan():执行plan这个方法,paln里需要写个构造函数constructor,new Plan()不仅创建paln这个实例,
            // 还把constructor这个方法执行,在这里面创建一个容器planList(但是这个容器其他三个方法里面都能用到),
            // 所以这个容器需要放到这个paln这个实例上。
        }
    }

    window.$ = window.Plan = Plan;
}();

// $.Callbacks();//$:plan  等价于  plan.Callbacks():相当于把plan当做一个对象设置属性Callbacks(),这里不是放在原型上的  是挂载到PLAN对象上的属性和方法

// let $plan = Plan.Callbacks();//jq中执行Callbacks()相当于创建了一个计划表
// // console.log($plan);
// $plan.add();
// $plan.remove();
// $plan.fire(100,200,300);



// 小测一下
let plan1 = $.Callbacks();

// console.dir(plan1);

let fn1 = function () {
    console.log(1,arguments);
}
plan1.add(fn1);

let fn2 = function () {
    console.log(2,arguments);
}
plan1.add(fn2);

let fn3 = function () {
    console.log(3);
    plan1.remove(fn1);
    plan1.remove(fn2);
}
plan1.add(fn3);

let fn4 = function () {
    console.log(4);
}
plan1.add(fn4);

plan1.fire(100,200);

 

数组塌陷

each方法的一点问题,也会引起数组塌陷

很多情况下都会引起数组塌陷问题,dom2兼容处理、封装发布订阅模式库都遇到过,

在一个方法中循环执行,在另外一个方法中把数组中一项删除掉,都会引起塌陷,比如:

回调函数也是,在当前each方法中循环数组,在回调函数中删除数组的某一项,下次执行each方法中也会塌陷

自己写一个for循环数组的时候,在另外一个函数中删除掉数组某一项,不做i--,下一次循环也会塌陷

循环数组的时候一定要避免数组塌陷问题

WEEK6-DAY4-js-callbacks.js

~function () {
    //=>EACH:遍历数组中每一项的内容
    let each = function (ary, callBack) {
        for (let i = 0; i < ary.length; i++) {
            let result = callBack && callBack(ary[i], i);
            if (result === false) break;
            if (result === 'DEL') i--;
        }
    };

    class Plan {
        constructor() {
            this.planList = [];
        }

        //=>挂载到PLAN原型上的方法
        add(fn) {
            let planList = this.planList,
                flag = true;
            each(planList, function (item, index) {
                if (item === fn) flag = false;
                return flag;
            });
            flag ? planList.push(fn) : null;
        }

        remove(fn) {
            let planList = this.planList;
            each(planList, function (item, index) {
                if (item === fn) {
                    planList[index] = null;
                    return false;
                }
            });
        }

        fire(...arg) {
            let planList = this.planList;
            each(planList, function (item, index) {
                if (item === null) {
                    planList.splice(index, 1);
                    return 'DEL';
                }
                item(...arg);
            });
        }

        //=>挂载到PLAN对象上的属性和方法
        static Callbacks() {
            return new Plan();
        }
    }

    window.$ = window.Plan = Plan;
}();

7、构建自己第一个插件-拖拽插件,可支持后期扩容

拖拽的主题核心就是mousedown、mousemove、mouseup,但是在每个阶段可能需要做一些其他事情(可能要做很多件事情,也可能不做,也可能只做一件事情),类似这种需求(做好多件事情不确定)可以通过回调函数解决,但是回调函数只能传递一个,而我们的发布订阅模式就正好满足这种需求,我们在计划表中扔需要做的事情(方法),当每个阶段需要执行的时候,通过触发这个计划表中的事件就可以实现

发布订阅可以融合到任何案列中,扩展性强

封装一个拖拽库,这个库里面用了发布订阅,也封装了事件池、each、BIND兼容处理等

WEEK6-DAY4-4-DRAG.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>珠峰培训</title>
    <link rel="stylesheet" href="css/reset.min.css">
    <style>
        html, body {
            height: 100%;
            overflow: hidden;
        }

        .container {
            position: relative;
            margin: 20px auto;
            width: 500px;
            height: 500px;
            border: 1px solid green;
        }

        .box {
            position: absolute;
            width: 100px;
            height: 100px;
            background: red;
            cursor: move;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="box" id="box"></div>
</div>


<script src="js/drag.js"></script>
<script>
    let temp = Drag.init(box); //<=> new Drag(box)

    // 按下变橙色
    temp.planDown.add(function (dragExp) {//通过add向计划表中增加方法 dragExp就是通过this传递过来的drag实例  通过dragExp.curEle获取当前拖拽的元素
        dragExp.curEle.style.background = 'orange';
    });

    // 抬起变红色
    temp.planUp.add(function (dragExp) {
        dragExp.curEle.style.background = 'red';
    });

    // 鼠标抬起让当前拖拽元素做自由落体运动  按下需要清除定时器
    temp.planDown.add(function (dragExp) {
        let curEle = dragExp.curEle;
        clearInterval(curEle.dropTimer);
    });
    temp.planUp.add(function (dragExp) {
        let curEle = dragExp.curEle;//dragExp就是通过this传递过来的drag实例  通过dragExp.curEle获取当前拖拽的元素
        let speed = 10,//速度
                flag = 0;//标识,当flag已经大于1 就清除掉这个定时器
        curEle.dropTimer = setInterval(function () {
            if (flag > 1) {
                clearInterval(curEle.dropTimer);
                return;
            }
            speed += 10;
            speed *= 0.98;//让速度一直消减
            let curT = curEle.offsetTop + speed;
            if (curT >= dragExp.maxT) {//边界判断  maxT在drag.js中做了挂载到实例maxL属性上的处理
                curT = dragExp.maxT;
                speed *= -1;//到底反弹
                flag++;
            } else {
                flag = 0;
            }
            curEle.style.top = curT + 'px';
        }, 17);
    });

    // 以后想基于拖拽做什么,就可以通过发布订阅(计划表)实现
</script>
</body>
</html>

WEEK6-DAY4-js-drag.js

//=>EACH:遍历数组中每一项的内容
let each = function (ary, callBack) {
    if (!(ary instanceof Array)) return;//如果ary不是数组就不执行这个each
    for (let i = 0; i < ary.length; i++) {
        let result = callBack && callBack(ary[i], i);
        if (result === false) break;
        if (result === 'DEL') i--;
    }
};


//=>BIND:兼容处理,预先改变THIS的
Function.prototype.myBind = function myBind(context = window, ...outer) {
    if ('bind' in this) {
        return this.bind(...arguments);
    }
    return (...inner)=> this.apply(context, outer.concat(inner));
};


//=>第一部分:基于DOM2事件绑定库  封装事件池
~function () {
    class EventLibrary {
        on(curEle, type, fn) {
            //=>this:example实例
            if (typeof curEle['pond' + type] === 'undefined') {
                curEle['pond' + type] = [];//创建事件池

                let _run = this.run;//获取run方法
                //把run方法放在内置事件池中  判断addEventListener兼不兼容,兼容就放在内置事件池addEventListener中,不兼容就放在内置事件池attachEvent中
                'addEventListener' in document ? curEle.addEventListener(type, _run, false) : curEle.attachEvent('on' + type, function (e) {
                    _run.call(curEle, e);// curEle.attachEvent('on' + type, _run)这样执行 , _run方法中的this是window , 
                    // 所以curEle.attachEvent('on' + type, function (e) {_run.call(curEle, e);这样去执行解决ie下this指向问题
                });
            }
            let ary = curEle['pond' + type],
                flag = true;
            each(ary, (item, index)=> {//遍历事件池,判断事件池中是否已经存在当前需要添加的方法  如果存在就不再添加(不再执行ary.push(fn))
                if (item === fn) flag = false;
                return flag;
            });
            flag ? ary.push(fn) : null;
        }

        off(curEle, type, fn) {
            let ary = curEle['pond' + type];
            each(ary, (item, index)=> {//遍历事件池,移除当前事件
                if (item === fn) {
                    ary[index] = null;
                }
            });
        }

        run(e) {
            //=>this:curEle当前元素,在on中绑定时已经处理成当前元素了
            e = e || window.event;
            if (!e.target) {//处理事件对象e的兼容问题
                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 ary = this['pond' + e.type];//run中的this在on中绑定时已经处理成当前元素了
            each(ary, (item, index)=> {//遍历事件池  执行传递进来的函数并且让this指向当前元素
                if (item === null) {
                    ary.splice(index, 1);
                    return 'DEL';
                }
                item.call(this, e);//让this指向当前元素
            });
        }
    }
    window.EventLibrary = EventLibrary;
}();
// 执行EventLibrary里的方法:
// let ev = new EventLibrary();
// ev.on(document.body,'click',fn1);


//=>第二部分:发布订阅库
~function () {
    class Plan {
        constructor() {
            this.planList = [];
        }

        add(fn) {
            let planList = this.planList,
                flag = true;
            each(planList, function (item, index) {
                if (item === fn) flag = false;
                return flag;
            });
            flag ? planList.push(fn) : null;
        }

        remove(fn) {
            let planList = this.planList;
            each(planList, function (item, index) {
                if (item === fn) {
                    planList[index] = null;
                    return false;
                }
            });
        }

        fire(...arg) {
            let planList = this.planList;
            each(planList, function (item, index) {
                if (item === null) {
                    planList.splice(index, 1);
                    return 'DEL';
                }
                item(...arg);
            });
        }

        static Callbacks() {
            return new Plan();
        }
    }
    window.Plan = Plan;
}();


//=>第三部分:拖拽库
~function () {
    class Drag extends EventLibrary {//创建一个类Drag继承EventLibrary才可以使用EventLibrary中的公有方法属性
        constructor(curEle) {
            super();//继承必须要写这一步

            //=>this:example实例
            this.curEle = curEle;//把当前需要操作的元素挂载到实例私有属性上,方便公有方法调用这个元素
            // 构造函数模式,如果想让每个方法都能用到一个变量值的话,需要把这个值存放到当前实例的私有属性上

            //=>CALL-BACKS  发布订阅  创建计划表存放到this.planDown、this.planMove、this.planUp
            this.planDown = Plan.Callbacks();
            this.planMove = Plan.Callbacks();
            this.planUp = Plan.Callbacks();

            //=>MOUSE-DOWN 给curEle绑定mousedown  继承了父级,所以能找到父级里面的公有方法
            // this.on(curEle, 'mousedown', this.down);//这样执行,dowm(e)方法中的this是curEle(因为on方法自动会把当前要执行的方法中的this指向
            // // 当前元素=>见item.call(this, e);//让this指向当前元素这一步处理 )
            //  为了让dowm(e)方法中的this指向实例(保证每个原型方法中的this是当前类的实例)  如下
            this.on(curEle, 'mousedown', this.down.myBind(this));
        }

        down(e) {
            //=>this:example实例
            let curEle = this.curEle,//获取当前元素,取得鼠标和盒子的起始位置并存到实例上(因为每次移动需要从新执行Drag.init()创建新的)
                {top:t, left:l}=this.offset(true);//获取盒子(当前example实例)起始值  传参数true代表获取的是相对于父级参照物的top和left值
            this.mouseX = e.pageX;
            this.mouseY = e.pageY;
            this.strL = l;
            this.strT = t;

            //=>MOUSE-MOVE / MOUSE-UP  防止鼠标焦点丢失,把这两个方法绑定给document,并且改变this指向

            // this.on(document, 'mousemove', this);//这样绑定,mouve和up中的this都不是实例,都是document
            // this.on(document, 'mouseup', this);

            // 用myBind处理this问题,但是需要把这个处理保存给当前元素私有属性上,移除才知道移除那个方法
            this._MOVE = this.move.myBind(this);
            this._UP = this.up.myBind(this);
            this.on(document, 'mousemove', this._MOVE);
            this.on(document, 'mouseup', this._UP);

            //=>FIRE 通知执行planDown计划表 
            // this.planDown.fire.call(this)//这样会把fire里面的this指向example实例(Drag),但是fire中的this必须要保证是Plan这个实例,所以这样写不行
            // 但是当按下操作完毕后执行这个计划表,需要操作当前example实例(Drag),所以把这个this(当前example实例(Drag))当参数传进去(因为不能通过call去改变this)
            this.planDown.fire(this);
        }

        move(e) {//计算盒子当前left和top值  随时获取最新鼠标位置减去原有鼠标位置加上原有盒子相对父级元素的top或者left值 
            // 但是得保证当前盒子是相对于父级元素来定位才行(如图:),之前讲的拖拽案列父级参照物都是body,这个案列中不是,这个案列
            // 父级参照物是如图中的大盒子

            //=>this:example
            let curEle = this.curEle;
            let curL = e.pageX - this.mouseX + this.strL,// 当前left 最新鼠标位置减去原有鼠标位置加上原有盒子相对父级元素的top或者left值 
                curT = e.pageY - this.mouseY + this.strT;

            //=>边界判断:此时的边界不再是一屏幕的宽高,而是父级的宽高减去盒子本身的宽高就是最大的left和top值,但是如果父级是body,此时边界就是
            // 一屏幕的宽高减去盒子本身的宽高
            let {offsetParent:p}=curEle,//获取当前元素的父级参照物
                W = document.documentElement.clientWidth || document.body.clientWidth,//默认获取一屏幕的宽高
                H = document.documentElement.clientHeight || document.body.clientHeight;
            if (p.tagName !== 'BODY') {//如果参照物父级不是body,获取当前元素父级的宽高
                W = p.clientWidth;
                H = p.clientHeight;
            }
            // 计算边界(最大可移动的宽高)
            let maxL = W - curEle.offsetWidth,
                maxT = H - curEle.offsetHeight;
            curL = curL < 0 ? 0 : (curL > maxL ? maxL : curL);
            curT = curT < 0 ? 0 : (curT > maxT ? maxT : curT);

            curEle.style.left = curL + 'px';//给这个元素设置left top值
            curEle.style.top = curT + 'px';

            this.maxL = maxL;//把maxL挂载到实例的maxL属性上,DARG.html中通过发布订阅去实现自由落体运动时,dragExp.maxT好获取到这个边界值
            this.maxT = maxT;
                
            //=>FIRE  通知执行planMove计划表
            this.planMove.fire(this);
        }

        up(e) {
            //=>this:example
            this.off(document, 'mousemove', this._MOVE);
            this.off(document, 'mouseup', this._UP);

            //=>FIRE  通知执行planUp计划表
            this.planUp.fire(this);
        }

        offset(flag) {//获取当前盒子偏移量   获取父级参照物、本身的上偏移、左偏移
            //=>this:example

            // let curEle = this.curEle;
            // let l = curEle.offsetLeft,
            //     t = curEle.offsetTop,
            //     p = curEle.offsetParent;

            let {offsetLeft:l, offsetTop:t, offsetParent:p}=this.curEle;
            if (!flag) {//flag为ture是获取参照物是父级元素的相对top和left值,不传或者传递false代表获取参照物是body元素的相对top和left
                while (p.tagName !== 'BODY') {//如果父级参照物还没有找到body  就一直往上找并且再原有的偏移量上计算(加上每次对应父级参照物的偏移量)
                    let {clientLeft, clientTop, offsetLeft, offsetTop, offsetParent}=p,
                        {userAgent}=window.navigator;
                    // if (window.navigator.userAgent.indexOf('MSIE 8') === -1) {
                    if (userAgent.indexOf('MSIE 8') === -1) {//不是ie8
                        l += clientLeft;
                        t += clientTop;
                    }
                    l += offsetLeft;
                    t += offsetTop;
                    p = offsetParent;//一直往上找 直到级参照物找到body为止
                }
            }
            return {top: t, left: l};
        }

        static init(curEle) { //通过init静态方法去返回一个Drag的实例(只有实例才可以去调这些公有方法,相当于es5实例调用原型上的方法)  
            // 这样Drag.init(oBox)就可以执行这个Drag方法,不再new Drag(oBox)
            return new Drag(curEle);
        }
        // static init(curEle, {index=0}={}) { //{index=0}={}配置参数的写法
        //     return new Drag(curEle);
        // }
    }
    window.Drag = Drag;
}();
// 执行这个Drag方法
// Drag.init(oBox,{
//     index:0
// });