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

一、一些對於技術發展的心得體會

學習各類設計模式有什麼做用?css

【開發】html

開發效率高jquery

利於團隊協做es6

【維護】ajax

有利於代碼的升級改變編程

邏輯清晰,代碼嚴謹,利於後期的維護bootstrap

【通用】設計模式

咱們依託設計模式能夠實現組件化、模塊化、插件化、框架化以及一些經常使用類庫方法的編寫數組

技術語言發展路線promise

語言語法更新迭代之路(路漫漫而其修遠兮)

語法初步穩定階段 -> 研究核心語法和使用的早一批人 ->封裝各類類庫和插件 ->大量研究核心的開發人員… ->出現各類設計模式 ->框架(VUE、REACT)

二、類庫、組件、插件、框架四者的區別

類庫:提供一些真實項目開發中經常使用的方法(方法作了完善處理:兼容處理、細節優化),方便咱們開發和維護 [ 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… ]

三、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結合一塊兒完成一個功能)

經常使用的設計模式基本上就是以上四個,還有更多設計模式

四、發佈訂閱設計模式的核心思想

俗稱叫作「觀察者模式」

實現思路和原理:

一、咱們先建立一個計劃表(容器)

二、後期須要作什麼事情,咱們都依次把須要處理的事情增長到計劃表中

三、當符合某個條件的時候,咱們只須要通知計劃表中的方法按照順序依次執行便可

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();
        }
    });


    // 可是這樣寫不方便後期維護,回調函數多嵌套了一層函數,而且若是回調函數裏須要執行的函數不
    // 僅僅只有三個時,就不方便維護(這樣寫必需要把當前動畫結束完成後須要執行的方法都寫出來),
    // 因此須要用發佈訂閱來解決,

    // 發佈訂閱的實現思路和原理:
    // 一、咱們先建立一個計劃表(容器) 
    // 二、後期須要作什麼事情,咱們都依次把須要處理的事情增長到計劃表中 
    // 三、當符合某個條件的時候,咱們只須要通知計劃表中的方法按照順序依次執行便可

    // 發佈訂閱跟dom2事件池兼容思想相似:
    // 一、先建立一個假事件池,
    // 二、全部點擊時候須要作的事情經過on先一個個放到事件池中可是並無執行,當要點擊時纔去執行,可是
    // 點擊時到底須要執行多少個方法並不知道,可是不要緊,想要執行多少個方法都經過on放到假事件池中去,
    // 三、當點擊時經過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>
複製代碼

五、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>
複製代碼

六、封裝屬於本身的發佈訂閱模式庫

封裝一個相似於jq的發佈訂閱模式庫

一、基於構造函數封裝

二、模擬JQ的操做步驟

三、注意數組塌陷問題

四、封裝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;
}();
複製代碼

七、構建本身第一個插件-拖拽插件,可支持後期擴容

拖拽的主題核心就是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
// });
複製代碼

相關文章
相關標籤/搜索