前端基礎進階(十):面向對象實戰之封裝拖拽對象

終於

前面幾篇文章,我跟你們分享了JavaScript的一些基礎知識,這篇文章,將會進入第一個實戰環節:利用前面幾章的所涉及到的知識,封裝一個拖拽對象。爲了可以幫助你們瞭解更多的方式與進行對比,我會使用三種不一樣的方式來實現拖拽。javascript

  • 不封裝對象直接實現;
  • 利用原生JavaScript封裝拖拽對象;
  • 經過擴展jQuery來實現拖拽對象。
本文的例子會放置於 codepen.io中,供你們在閱讀時直接查看。若是對於codepen不瞭解的同窗,能夠花點時間稍微瞭解一下。

拖拽的實現過程會涉及到很是多的實用小知識,所以爲了鞏固我本身的知識積累,也爲了你們可以學到更多的知識,我會盡可能詳細的將一些細節分享出來,相信你們認真閱讀以後,必定能學到一些東西。css

一、如何讓一個DOM元素動起來

咱們經常會經過修改元素的top,left,translate來其的位置發生改變。在下面的例子中,每點擊一次按鈕,對應的元素就會移動5px。你們可點擊查看。前端

點擊查看一個讓元素動起來的小例子java

因爲修改一個元素top/left值會引發頁面重繪,而translate不會,所以從性能優化上來判斷,咱們會優先使用translate屬性。
二、如何獲取當前瀏覽器支持的transform兼容寫法

transform是css3的屬性,當咱們使用它時就不得不面對兼容性的問題。不一樣版本瀏覽器的兼容寫法大體有以下幾種:css3

['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'OTransform']web

所以咱們須要判斷當前瀏覽器環境支持的transform屬性是哪種,方法以下:segmentfault

// 獲取當前瀏覽器支持的transform兼容寫法
function getTransform() {
    var transform = '',
        divStyle = document.createElement('div').style,
        // 可能涉及到的幾種兼容性寫法,經過循環找出瀏覽器識別的那一個
        transformArr = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'OTransform'],

        i = 0,
        len = transformArr.length;

    for(; i < len; i++)  {
        if(transformArr[i] in divStyle) {
            // 找到以後當即返回,結束函數
            return transform = transformArr[i];
        }
    }

    // 若是沒有找到,就直接返回空字符串
    return transform;
}

該方法用於獲取瀏覽器支持的transform屬性。若是返回的爲空字符串,則表示當前瀏覽器並不支持transform,這個時候咱們就須要使用left,top值來改變元素的位置。若是支持,就改變transform的值。瀏覽器

三、 如何獲取元素的初始位置

咱們首先須要獲取到目標元素的初始位置,所以這裏咱們須要一個專門用來獲取元素樣式的功能函數。性能優化

可是獲取元素樣式在IE瀏覽器與其餘瀏覽器有一些不一樣,所以咱們須要一個兼容性的寫法。閉包

function getStyle(elem, property) {
    // ie經過currentStyle來獲取元素的樣式,其餘瀏覽器經過getComputedStyle來獲取
    return document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(elem, false)[property] : elem.currentStyle[property];
}

有了這個方法以後,就能夠開始動手寫獲取目標元素初始位置的方法了。

function getTargetPos(elem) {
    var pos = {x: 0, y: 0};
    var transform = getTransform();
    if(transform) {
        var transformValue = getStyle(elem, transform);
        if(transformValue == 'none') {
            elem.style[transform] = 'translate(0, 0)';
            return pos;
        } else {
            var temp = transformValue.match(/-?\d+/g);
            return pos = {
                x: parseInt(temp[4].trim()),
                y: parseInt(temp[5].trim())
            }
        }
    } else {
        if(getStyle(elem, 'position') == 'static') {
            elem.style.position = 'relative';
            return pos;
        } else {
            var x = parseInt(getStyle(elem, 'left') ? getStyle(elem, 'left') : 0);
            var y = parseInt(getStyle(elem, 'top') ? getStyle(elem, 'top') : 0);
            return pos = {
                x: x,
                y: y
            }
        }
    }
}

在拖拽過程當中,咱們須要不停的設置目標元素的新位置,這樣它纔會移動起來,所以咱們須要一個設置目標元素位置的方法。

// pos = { x: 200, y: 100 }
function setTargetPos(elem, pos) {
    var transform = getTransform();
    if(transform) {
        elem.style[transform] = 'translate('+ pos.x +'px, '+ pos.y +'px)';
    } else {
        elem.style.left = pos.x + 'px';
        elem.style.top = pos.y + 'px';
    }
    return elem;
}
五、咱們須要用到哪些事件?

在pc上的瀏覽器中,結合mousedown、mousemove、mouseup這三個事件能夠幫助咱們實現拖拽。

  • mousedown 鼠標按下時觸發
  • mousemove 鼠標按下後拖動時觸發
  • mouseup 鼠標鬆開時觸發
而在移動端,分別與之對應的則是 touchstart、touchmove、touchend

當咱們將元素綁定這些事件時,有一個事件對象將會做爲參數傳遞給回調函數,經過事件對象,咱們能夠獲取到當前鼠標的精確位置,鼠標位置信息是實現拖拽的關鍵。

事件對象十分重要,其中包含了很是多的有用的信息,這裏我就不擴展了,你們能夠在函數中將事件對象打印出來查看其中的具體屬性,這個方法對於記不清事件對象重要屬性的童鞋很是有用。
六、拖拽的原理

當事件觸發時,咱們能夠經過事件對象獲取到鼠標的精切位置。這是實現拖拽的關鍵。當鼠標按下(mousedown觸發)時,咱們須要記住鼠標的初始位置與目標元素的初始位置,咱們的目標就是實現當鼠標移動時,目標元素也跟着移動,根據常理咱們能夠得出以下關係:

移動後的鼠標位置 - 鼠標初始位置 = 移動後的目標元素位置 - 目標元素的初始位置

若是鼠標位置的差值咱們用dis來表示,那麼目標元素的位置就等於:

移動後目標元素的位置 = dis + 目標元素的初始位置

經過事件對象,咱們能夠精確的知道鼠標的當前位置,所以當鼠標拖動(mousemove)時,咱們能夠不停的計算出鼠標移動的差值,以此來求出目標元素的當前位置。這個過程,就實現了拖拽。

而在鼠標鬆開(mouseup)結束拖拽時,咱們須要處理一些收尾工做。詳情見代碼。

七、 我又來推薦思惟導圖輔助寫代碼了

經常有新人朋友跑來問我,若是邏輯思惟能力不強,能不能寫代碼作前端。個人答案是:能。由於藉助思惟導圖,能夠很輕鬆的彌補邏輯的短板。並且比在本身頭腦中腦補邏輯更加清晰明瞭,不易出錯。

上面第六點我介紹了原理,所以如何作就顯得不是那麼難了,而具體的步驟,則在下面的思惟導圖中明確給出,咱們只須要按照這個步驟來寫代碼便可,試試看,必定很輕鬆。

使用思惟導圖清晰的表達出整個拖拽過程咱們須要乾的事情

八、代碼實現

part一、準備工做

// 獲取目標元素對象
var oElem = document.getElementById('target');

// 聲明2個變量用來保存鼠標初始位置的x,y座標
var startX = 0;
var startY = 0;

// 聲明2個變量用來保存目標元素初始位置的x,y座標
var sourceX = 0;
var sourceY = 0;

part二、功能函數

由於以前已經貼過代碼,就再也不重複

// 獲取當前瀏覽器支持的transform兼容寫法
function getTransform() {}

// 獲取元素屬性
function getStyle(elem, property) {}

// 獲取元素的初始位置
function getTargetPos(elem) {}

// 設置元素的初始位置
function setTargetPos(elem, potions) {}

part三、聲明三個事件的回調函數

這三個方法就是實現拖拽的核心所在,我將嚴格按照上面思惟導圖中的步驟來完成咱們的代碼。

// 綁定在mousedown上的回調,event爲傳入的事件對象
function start(event) {
    // 獲取鼠標初始位置
    startX = event.pageX;
    startY = event.pageY;

    // 獲取元素初始位置
    var pos = getTargetPos(oElem);

    sourceX = pos.x;
    sourceY = pos.y;

    // 綁定
    document.addEventListener('mousemove', move, false);
    document.addEventListener('mouseup', end, false);
}

function move(event) {
    // 獲取鼠標當前位置
    var currentX = event.pageX;
    var currentY = event.pageY;

    // 計算差值
    var distanceX = currentX - startX;
    var distanceY = currentY - startY;

    // 計算並設置元素當前位置
    setTargetPos(oElem, {
        x: (sourceX + distanceX).toFixed(),
        y: (sourceY + distanceY).toFixed()
    })
}

function end(event) {
    document.removeEventListener('mousemove', move);
    document.removeEventListener('mouseup', end);
    // do other things
}

OK,一個簡單的拖拽,就這樣愉快的實現了。點擊下面的連接,能夠在線查看該例子的demo。

使用原生js實現拖拽

九、封裝拖拽對象

在前面一章我給你們分享了面向對象如何實現,基於那些基礎知識,咱們來將上面實現的拖拽封裝爲一個拖拽對象。咱們的目標是,只要咱們聲明一個拖拽實例,那麼傳入的目標元素將自動具有能夠被拖拽的功能。

在實際開發中,一個對象咱們經常會單獨放在一個js文件中,這個js文件將單獨做爲一個模塊,利用各類模塊的方式組織起來使用。固然這裏沒有複雜的模塊交互,由於這個例子,咱們只須要一個模塊便可。

爲了不變量污染,咱們須要將模塊放置於一個函數自執行方式模擬的塊級做用域中。

;
(function() {
    ...
})();
在普通的模塊組織中,咱們只是單純的將許多js文件壓縮成爲一個js文件,所以此處的第一個分號則是爲了防止上一個模塊的結尾不用分號致使報錯。必不可少。固然在經過require或者ES6模塊等方式就不會出現這樣的狀況。

咱們知道,在封裝一個對象的時候,咱們能夠將屬性與方法放置於構造函數或者原型中,而在增長了自執行函數以後,咱們又能夠將屬性和方法防止與模塊的內部做用域。這是閉包的知識。

那麼咱們面臨的挑戰就在於,如何合理的處理屬性與方法的位置。

固然,每個對象的狀況都不同,不能一律而論,咱們須要清晰的知道這三種位置的特性才能作出最適合的決定。

  • 構造函數中: 屬性與方法爲當前實例單獨擁有,只能被當前實例訪問,而且每聲明一個實例,其中的方法都會被從新建立一次。
  • 原型中: 屬性與方法爲全部實例共同擁有,能夠被全部實例訪問,新聲明實例不會重複建立方法。
  • 模塊做用域中:屬性和方法不能被任何實例訪問,可是能被內部方法訪問,新聲明的實例,不會重複建立相同的方法。

對於方法的判斷比較簡單。

由於在構造函數中的方法總會在聲明一個新的實例時被重複建立,所以咱們聲明的方法都儘可能避免出如今構造函數中。

而若是你的方法中須要用到構造函數中的變量,或者想要公開,那就須要放在原型中。

若是方法須要私有不被外界訪問,那麼就放置在模塊做用域中。

對於屬性放置於什麼位置有的時候很難作出正確的判斷,所以我很難給出一個準確的定義告訴你什麼屬性必定要放在什麼位置,這須要在實際開發中不斷的總結經驗。可是總的來講,仍然要結合這三個位置的特性來作出最合適的判斷。

若是屬性值只能被實例單獨擁有,好比person對象的name,只能屬於某一個person實例,又好比這裏拖拽對象中,某一個元素的初始位置,也僅僅只是這個元素的當前位置,這個屬性,則適合放在構造函數中。

而若是一個屬性僅僅供內部方法訪問,這個屬性就適合放在模塊做用域中。

關於面向對象,上面的幾點思考我認爲是這篇文章最值得認真思考的精華。若是在封裝時沒有思考清楚,極可能會遇到不少你意想不到的bug,因此建議你們結合本身的開發經驗,多多思考,總結出本身的觀點。

根據這些思考,你們能夠本身嘗試封裝一下。而後與個人作一些對比,看看咱們的想法有什麼不一樣,在下面例子的註釋中,我將本身的想法表達出來。

點擊查看已經封裝好的demo

js 源碼

;
(function() {
    // 這是一個私有屬性,不須要被實例訪問
    var transform = getTransform();

    function Drag(selector) {
        // 放在構造函數中的屬性,都是屬於每個實例單獨擁有
        this.elem = typeof selector == 'Object' ? selector : document.getElementById(selector);
        this.startX = 0;
        this.startY = 0;
        this.sourceX = 0;
        this.sourceY = 0;

        this.init();
    }


    // 原型
    Drag.prototype = {
        constructor: Drag,

        init: function() {
            // 初始時須要作些什麼事情
            this.setDrag();
        },

        // 稍做改造,僅用於獲取當前元素的屬性,相似於getName
        getStyle: function(property) {
            return document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(this.elem, false)[property] : this.elem.currentStyle[property];
        },

        // 用來獲取當前元素的位置信息,注意與以前的不一樣之處
        getPosition: function() {
            var pos = {x: 0, y: 0};
            if(transform) {
                var transformValue = this.getStyle(transform);
                if(transformValue == 'none') {
                    this.elem.style[transform] = 'translate(0, 0)';
                } else {
                    var temp = transformValue.match(/-?\d+/g);
                    pos = {
                        x: parseInt(temp[4].trim()),
                        y: parseInt(temp[5].trim())
                    }
                }
            } else {
                if(this.getStyle('position') == 'static') {
                    this.elem.style.position = 'relative';
                } else {
                    pos = {
                        x: parseInt(this.getStyle('left') ? this.getStyle('left') : 0),
                        y: parseInt(this.getStyle('top') ? this.getStyle('top') : 0)
                    }
                }
            }

            return pos;
        },

        // 用來設置當前元素的位置
        setPostion: function(pos) {
            if(transform) {
                this.elem.style[transform] = 'translate('+ pos.x +'px, '+ pos.y +'px)';
            } else {
                this.elem.style.left = pos.x + 'px';
                this.elem.style.top = pos.y + 'px';
            }
        },

        // 該方法用來綁定事件
        setDrag: function() {
            var self = this;
            this.elem.addEventListener('mousedown', start, false);
            function start(event) {
                self.startX = event.pageX;
                self.startY = event.pageY;

                var pos = self.getPosition();

                self.sourceX = pos.x;
                self.sourceY = pos.y;

                document.addEventListener('mousemove', move, false);
                document.addEventListener('mouseup', end, false);
            }

            function move(event) {
                var currentX = event.pageX;
                var currentY = event.pageY;

                var distanceX = currentX - self.startX;
                var distanceY = currentY - self.startY;

                self.setPostion({
                    x: (self.sourceX + distanceX).toFixed(),
                    y: (self.sourceY + distanceY).toFixed()
                })
            }

            function end(event) {
                document.removeEventListener('mousemove', move);
                document.removeEventListener('mouseup', end);
                // do other things
            }
        }
    }

    // 私有方法,僅僅用來獲取transform的兼容寫法
    function getTransform() {
        var transform = '',
            divStyle = document.createElement('div').style,
            transformArr = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'OTransform'],

            i = 0,
            len = transformArr.length;

        for(; i < len; i++)  {
            if(transformArr[i] in divStyle) {
                return transform = transformArr[i];
            }
        }

        return transform;
    }

    // 一種對外暴露的方式
    window.Drag = Drag;
})();

// 使用:聲明2個拖拽實例
new Drag('target');
new Drag('target2');

這樣一個拖拽對象就封裝完畢了。

建議你們根據我提供的思惟方式,多多嘗試封裝一些組件。好比封裝一個彈窗,封裝一個循環輪播等。練得多了,面向對象就再也不是問題了。這種思惟方式,在將來任什麼時候候都是可以用到的。

下一章分析jQuery對象的實現,與如何將咱們這裏封裝的拖拽對象擴展爲jQuery插件。

前端基礎進階系列目錄

clipboard.png

相關文章
相關標籤/搜索