基於"發佈-訂閱"的原生JS插件封裝

你們好,我是神三元。 今天咱們來作一個小玩意,用原生JS封裝一個動畫插件。效果以下:javascript

這個飛馳的小球看起來是否是特有靈性呢?沒錯,它就是用原生JS實現的。 接下來,就讓咱們深刻細節,體會其中的奧祕。相信這個實現的過程,會比動畫自己更加精彩!

1、需求分析

封裝一個插件,將小球的DOM對象做爲參數傳入,使得小球在鼠標按下和放開後可以運動,在水平方向作勻減速直線運動,初速度爲鼠標移開瞬間的速度,在豎直方向的運動相似於自由落體運動。而且,小球的始終在不離開瀏覽器的邊界運動,碰到邊界會有如圖的反彈效果。css

2、梳理思路

分析這樣的一個過程,其中大體會經歷一下的關鍵步驟:html

  • 一、鼠標按下時,記錄小球的初始位置信息
  • 二、鼠標按下後滑動,記錄鬆開鼠標瞬間的移動速度
  • 三、鼠標鬆開後,在水平方向上,讓小球根據剛剛記錄的移動速度進行勻減速運動,豎直方向設定一個豎直向下的加速度,開始運動。
  • 四、水平方向速度減爲0時,水平方向運動中止;豎直方向速度減爲0或者足夠小時,豎直方向運動中止。

3、難點分析

看到這裏,估計你的思路清晰了很多,但可能仍是有一些比較難以搞定的問題。vue

首先,你怎麼拿到鬆開手瞬間的小球移動速度?如何去表達出這個加速度的效果?java

在實現方面,這是很是重要的問題。不過,其實很是的簡單。程序員

瀏覽器自己就是存在反應時間的,你能夠把它當作一個攝像機,在給DOM元素綁定了事件以後,每隔一段時間(通常很是的短,根據不一樣瀏覽器廠商和電腦性能而定,這裏我用到chrome,保守估計爲20ms)會給這個元素拍張照,記錄它的狀態。在按下鼠標以後的拖動過程當中,事實上會給元素拍攝無數張照片。若是如今每通過一段時間,我記錄當下當前照片與上一段照片的位置差,那麼最後一次拍照和倒數第二次拍照的小球位置差距,是否是就能夠做爲離開的瞬時速度呢?固然能夠啦。廢話很少說,上圖:chrome

一樣,對實現加速度的效果,首先弄清一個問題,什麼是速度?速度就是單位時間內運動的距離,這裏暫且把它當作20ms內的距離,那麼我每次拍照時,將這個距離增長或減小一個值,這個值就是加速度。編程

4、初步實現

當大部分問題考慮清楚以後,如今開始實現。 首先是基本的樣式,比較簡單。數組

<!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; } #box{ position: absolute; top: 100px; left: 100px; width: 150px; height: 150px; border-radius: 50%; background: lightcoral; cursor: move; z-index: 0; } </style>
</head>
<body>
    <div id="box"></div>
</body>
</html>
複製代碼

如今來完成核心的JS代碼,採用ES6語法瀏覽器

//drag.js
class Drag {
    //ele爲傳入的DOM對象
    constructor(ele) {
            //初始化參數
        this.ele = ele;
        ['strX', 'strY', 'strL', 'strT', 'curL', 'curT'].forEach(item => {
            this[item] = null;
        });
        //爲按下鼠標綁定事件,事件函數必定要綁定this,在封裝過程當中this統一指定爲實例對象,下不贅述
        this.DOWN = this.down.bind(this);
        this.ele.addEventListener('mousedown', this.DOWN);
    }
    down(ev) {
        let ele = this.ele;
        this.strX = ev.clientX;//鼠標點擊處到瀏覽器窗口最左邊的距離
        this.strY = ev.clientY;//鼠標點擊處到瀏覽器窗口最上邊的距離
        this.strL = ele.offsetLeft;//元素到瀏覽器窗口最左邊的距離
        this.strT = ele.offsetTop;//元素到瀏覽器窗口最上邊的距離
    
        this.MOVE = this.move.bind(this);
        this.UP = this.up.bind(this);
        document.addEventListener('mousemove', this.MOVE);
        document.addEventListener('mouseup', this.UP);
        
        //flag
        //清理上一次點擊造成的一些定時器和變量
        clearInterval(this.flyTimer);
        this.speedFly = undefined;
        clearInterval(this.dropTimer);
    }
    move(ev) {
        let ele = this.ele;
        this.curL = ev.clientX - this.strX + this.strL;
        this.curT = ev.clientY - this.strY + this.strT;
        ele.style.left = this.curL + 'px';
        ele.style.top = this.curT + 'px';
        
        //flag
        //功能: 記錄鬆手瞬間小球的速度
        if (!this.lastFly) {
            this.lastFly = ele.offsetLeft;
            this.speedFly = 0;
            return;
        }
        this.speedFly = ele.offsetLeft - this.lastFly;
        this.lastFly = ele.offsetLeft;
    }
    up(ev) {
        //給前兩個事件解綁
        document.removeEventListener('mousemove', this.MOVE);
        document.removeEventListener('mouseup', this.UP);
        
        //flag
        //水平方向
        this.horizen.call(this);
        this.vertical.call(this);
    }
    //水平方向的運動
    horizen() {
        let minL = 0,
            maxL = document.documentElement.clientWidth - this.ele.offsetWidth;
        let speed = this.speedFly;
        speed = Math.abs(speed);
        this.flyTimer = setInterval(() => {
            speed *= .98;
            Math.abs(speed) <= 0.1 ? clearInterval(this.flyTimer):null;
            //小球當前到視口最左端的距離
            let curT = this.ele.offsetLeft;
            curT += speed;
            //小球到達視口最右端,反彈
            if (curT >= maxL) {
                this.ele.style.left = maxL + 'px';
                speed *= -1;
                return;
            }
            //小球到達視口最右端,反彈
            if (curT <= minL) {
                this.ele.style.left = minL + 'px';
                speed *= -1;
                return;
            }
            this.ele.style.left = curT + 'px';
        }, 20);
    }
    //豎直方向的運動
    vertical() {
        let speed = 9.8,
            minT = 0,
            maxT = document.documentElement.clientHeight - this.ele.offsetHeight,
            flag = 0;
        this.dropTimer = setInterval(() => {
            speed += 10;
            speed *= .98;
            Math.abs(speed) <= 0.1 ? clearInterval(this.dropTimer):null
            //小球當前到視口最左端的距離
            let curT = this.ele.offsetTop;
            curT += speed;
            //小球飛到視口頂部,反彈
            if (curT >= maxT) {
                this.ele.style.top = maxT + 'px';
                speed *= -1;
                return;
            }
            //小球落在視口底部,反彈
            if (curT <= minT) {
                this.ele.style.top = minT + 'px';
                speed *= -1;
                return;
            }
            this.ele.style.top = curT + 'px';
        }, 20);
    }
}
window.Drag = Drag;
複製代碼

到此,完整的效果就出來了,你能夠本身複製體驗一下。

4、採用發佈-訂閱

估計讀完這段代碼,你也體會到了這個功能的實現是很是容易實現的。可是實際上,做爲一個插件的標準來說,這段代碼是存在一些潛在的問題的,這些問題並非邏輯上的問題,而是設計問題。直白一點說,實際上是它的擴展性不強,假若我要對某一個效果進行從新調整或者直接重寫效果,我須要再這繁重的代碼裏面去搜索和修改。

所以,咱們這裏的目的並不僅是提供一個功能,它毫不只是一個玩具,咱們應當思考,如何將它作的更有通用性,可以獲得最大程度的複用。 這裏,我想引用軟件工程領域耳熟能詳的SOLID設計原則中的O部分————開放封閉原則。

開放封閉原則主要體如今兩個方面:
對擴展開放,意味着有新的需求或變化時,能夠對現有代碼進行擴展,以適應新的狀況。
對修改封閉,意味着類一旦設計完成,就能夠獨立完成其工做,而不要對類進行任何修改。
複製代碼

咱們但願儘量少地對類自己進行修改,由於你沒法預測具體的功能會如何變化。

那怎麼解決這個問題呢?很簡單,對擴展開放,咱們就將具體的效果代碼以擴展的方式提供,對類擴展,而不是所有放在類裏面。 咱們的具體作法就是採用發佈-訂閱模式。

發佈—訂閱模式又叫觀察者模式,它定義對象間的一對多的依賴關係,當一個對象的狀態發生改變時,全部依賴於它的對象都將獲得通知。
複製代碼

拿剛剛實現的功能來講,在對象建立的時候,我就開闢一個池子,將須要執行的方法放進這個池子,當鼠標按下的時候,我把池子裏面的函數拿過來依次執行,對於鼠標鬆開就再建立一個池子,同理,這就是發佈-訂閱。

jQuery裏面有現成的發佈訂閱方法。

//開闢一個容器
let $plan =  $.callBack();
//往容器裏面添加函數
$plan.add(function(x, y){
    console.log(x, y);
})
$plan.add(function(x, y){
    console.log(y, x);
})
$plan.fire(10, 20);//會輸出10,20 20,10
//$plan.remove(function)用來從容器中刪除某個函數
複製代碼

如今咱們不妨原生JS手寫一下簡單的發佈-訂閱,讓咱們原生擼到底

//subscribe.js
class Subscribe {
    constructor() {
        //建立容器
        this.pond = [];
    }
    //向容器中增長方法,注意去重
    add(fn) {
        let pond = this.pond,
            isExist = false;
        //去重環節
        pond.forEach(item => item === fn ? isExist = true : null);
        !isExist ? pond.push(fn) : null;
    }
    remove(fn) {
        let pond = this.pond;
        pond.forEach((item, index) => {
            if(item === fn) {
                //提一下我在這裏遇到的坑,這裏若是寫item=null是無效的
                //例子:let a = {name: funtion(){}};
                //let b = a.name;
                //這個時候操做b的值對於a的name屬性是沒有影響的
                pond[index] = null;
            }
        })
    }
    fire(...arg) {
        let pond = this.pond;
        for(let i = 0; i < pond.length; i++) {
            let item = pond[i];
            //若是itme爲空了,最好把它刪除掉
            if (item === null) {
                pond.splice(i, 1);
                //若是用了splice要防止數組塌陷問題,即刪除了一個元素後,後面全部元素的索引默認都會減1
                i--;
                continue;
            }
            item(...arg);
        }
    }
}
window.Subscribe = Subscribe;
複製代碼
//測試一下
let subscribe = new Subscribe();
let fn1 = function fn1(x, y) {
    console.log(1, x, y);
};
let fn2 = function fn2() {
    console.log(2);
};
let fn3 = function fn3() {
    console.log(3);
    subscribe.remove(fn1);
    subscribe.remove(fn2);
};
let fn4 = function fn4() {
    console.log(4);
};

subscribe.add(fn1);
subscribe.add(fn1);
subscribe.add(fn2);
subscribe.add(fn1);
subscribe.add(fn3);
subscribe.add(fn4);
setInterval(() => {
    subscribe.fire(100, 200);
}, 1000);
複製代碼

結果:

肯定過眼神,你就是對的Subscribe。(手動滑稽)

5、優化代碼

//Drag.js
if (typeof Subscribe === 'undefined') {
    throw new ReferenceError('沒有引入subscribe.js!');
}

class Drag {
    constructor(ele) {
        this.ele = ele;
        ['strX', 'strY', 'strL', 'strT', 'curL', 'curT'].forEach(item => {
            this[item] = null;
        });
        
        this.subDown = new Subscribe;
        this.subMove = new Subscribe;
        this.subUp = new Subscribe;

        //=>DRAG-START
        this.DOWN = this.down.bind(this);
        this.ele.addEventListener('mousedown', this.DOWN);
    }

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

        this.MOVE = this.move.bind(this);
        this.UP = this.up.bind(this);
        document.addEventListener('mousemove', this.MOVE);
        document.addEventListener('mouseup', this.UP);

        this.subDown.fire(ele, ev);
    }

    move(ev) {
        let ele = this.ele;
        this.curL = ev.clientX - this.strX + this.strL;
        this.curT = ev.clientY - this.strY + this.strT;
        ele.style.left = this.curL + 'px';
        ele.style.top = this.curT + 'px';

        this.subMove.fire(ele, ev);
    }

    up(ev) {
        document.removeEventListener('mousemove', this.MOVE);
        document.removeEventListener('mouseup', this.UP);

        this.subUp.fire(this.ele, ev);
    }
}

window.Drag = Drag;
複製代碼
//dragExtend.js
function extendDrag(drag) {
    //鼠標按下
    let stopAnimate = function stopAnimate(curEle) {
        clearInterval(curEle.flyTimer);
        curEle.speedFly = undefined;
        clearInterval(curEle.dropTimer);
    };
    //鼠標移動
    let computedFly = function computedFly(curEle) {
        if (!curEle.lastFly) {
            curEle.lastFly = curEle.offsetLeft;
            curEle.speedFly = 0;
            return;
        }
        curEle.speedFly = curEle.offsetLeft - curEle.lastFly;
        curEle.lastFly = curEle.offsetLeft;
    };
    //水平方向的運動
    let animateFly = function animateFly(curEle) {
        let minL = 0,
            maxL = document.documentElement.clientWidth - curEle.offsetWidth,
            speed = curEle.speedFly;
        curEle.flyTimer = setInterval(() => {
            speed *= .98;
            Math.abs(speed) <= 0.1 ? clearInterval(animateFly):null;
            let curT = curEle.offsetLeft;
            curT += speed;
            if (curT >= maxL) {
                curEle.style.left = maxL + 'px';
                speed *= -1;
                return;
            }
            if (curT <= minL) {
                curEle.style.left = minL + 'px';
                speed *= -1;
                return;
            }
            curEle.style.left = curT + 'px';
        }, 20);
    };
    //豎直方向的運動
    let animateDrop = function animateDrop(curEle) {
        let speed = 9.8,
            minT = 0,
            maxT = document.documentElement.clientHeight - curEle.offsetHeight;
        curEle.dropTimer = setInterval(() => {
            speed += 10;
            speed *= .98;
            Math.abs(speed) <= 0.1 ? clearInterval(animateFly):null;
            let curT = curEle.offsetTop;
            curT += speed;
            if (curT >= maxT) {
                curEle.style.top = maxT + 'px';
                speed *= -1;
                return;
            }
            if (curT <= minT) {
                curEle.style.top = minT + 'px';
                speed *= -1;
                return;
            }
            curEle.style.top = curT + 'px';
        }, 20);
    };
    drag.subDown.add(stopAnimate);
    drag.subMove.add(computedFly);
    drag.subUp.add(animateFly);
    drag.subUp.add(animateDrop);
};
複製代碼

在html文件中加入以下script

<script> //原生JS 小技巧: //直接寫box跟document.getElementById('box')是同樣的效果 let drag = new Drag(box); extendDrag(drag); </script>
複製代碼

接下來,你就能從新看到那個活潑的小球啦。

6、結(chui)語(niu)

恭喜你,讀到了這裏,至關不容易啊。先爲你點個贊!

在這裏我並非簡單講講效果的實現、貼貼代碼就過去了,而是帶你體驗了封裝插件的整個過程。有了發佈-訂閱的場景,理解這個設計思想就更加容易了。其實你看在這個過程當中,功能並無添加多少,可是這波操做確實值得,由於它讓整個代碼更加的靈活。回過頭看,好比DOM2的事件池機制,vue的生命週期鉤子等等,你就會明白它們爲何要這麼設計,原理上和此次封裝沒有區別,這樣一想,不少東西就更加清楚了。

在我看來,不管你是作哪一個端的開發工做,其實大部分業務場景、大部分流行的框架技術都極可能會在若干年後隨風而逝,但真正留下來的、伴隨你一輩子的東西是編程思想。在個人理解中,編程的意義遠不止造輪子,寫插件,來顯得本身金玉其外,而是留心思考,提煉出一些思考問題的方式,從而在某個肯定的時間點讓你擁有極其敏銳的判斷,來指導和優化你下一步的決策,而不是縱身於飛速迭代的技術浪潮,日漸焦慮。我以爲這是一個程序員應該追求的東西。

相關文章
相關標籤/搜索