1. 程式人生 > >DOM0事件和DOM2事件模型 —— JS中的設計模式和元件封裝

DOM0事件和DOM2事件模型 —— JS中的設計模式和元件封裝

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
// });
複製程式碼