詳解JavaScript面向對象實戰之封裝拖拽對象

概述

為瞭能夠幫助大傢瞭解更多的方式與進行對比,我會使用三種不同的方式來實現拖拽。

  • 不封裝對象直接實現;
  • 利用原生JavaScript封裝拖拽對象;
  • 通過擴展jQuery來實現拖拽對象。

拖拽的實現過程會涉及到非常多的實用小知識,因此為瞭鞏固我自己的知識積累,也為瞭大傢能夠學到更多的知識,我會盡量詳細的將一些細節分享出來,相信大傢認真閱讀之後,一定能學到一些東西。

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

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

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

由於修改一個元素top/left值會引起頁面重繪,而translate不會,因此從性能優化上來判斷,我們會優先使用translate屬性。

2、如何獲取當前瀏覽器支持的transform兼容寫法

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

[‘transform’, ‘webkitTransform’, ‘MozTransform’, ‘msTransform’, ‘OTransform’]

因此我們需要判斷當前瀏覽器環境支持的transform屬性是哪一種,方法如下:

// 獲取當前瀏覽器支持的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的值。

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

我們首先需要獲取到目標元素的初始位置,因此這裡我們需要一個專門用來獲取元素樣式的功能函數。

但是獲取元素樣式在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;
}

5、我們需要用到哪些事件?

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

  • mousedown鼠標按下時觸發
  • mousemove鼠標按下後拖動時觸發
  • mouseup鼠標松開時觸發

而在移動端,分別與之對應的則是touchstart、touchmove、touchend。

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

事件對象十分重要,其中包含瞭非常多的有用的信息,這裡我就不擴展瞭,大傢可以在函數中將事件對象打印出來查看其中的具體屬性,這個方法對於記不清事件對象重要屬性的童鞋非常有用。

6、拖拽的原理

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

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

如果鼠標位置的差值我們用dis來表示,那麼目標元素的位置就等於:

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

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

而在鼠標松開(mouseup)結束拖拽時,我們需要處理一些收尾工作。詳情見代碼。

7、 我又來推薦思維導圖輔助寫代碼瞭

常常有新人朋友跑來問我,如果邏輯思維能力不強,能不能寫代碼做前端。我的答案是:能。因為借助思維導圖,可以很輕松的彌補邏輯的短板。而且比在自己頭腦中腦補邏輯更加清晰明瞭,不易出錯。

上面第六點我介紹瞭原理,因此如何做就顯得不是那麼難瞭,而具體的步驟,則在下面的思維導圖中明確給出,我們隻需要按照這個步驟來寫代碼即可,試試看,一定很輕松。

使用思維導圖清晰的表達出整個拖拽過程我們需要幹的事情

8、代碼實現

part1、準備工作

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

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

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

part2、功能函數

因為之前已經貼過代碼,就不再重復

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

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

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

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

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

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

// 綁定在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實現拖拽

9、封裝拖拽對象

我們來將上面實現的拖拽封裝為一個拖拽對象。我們的目標是,隻要我們聲明一個拖拽實例,那麼傳入的目標元素將自動具備可以被拖拽的功能。

在實際開發中,一個對象我們常常會單獨放在一個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');

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

建議大傢根據我提供的思維方式,多多嘗試封裝一些組件。比如封裝一個彈窗,封裝一個循環輪播等。練得多瞭,面向對象就不再是問題瞭。這種思維方式,在未來任何時候都是能夠用到的。

以上就是詳解JavaScript面向對象實戰之封裝拖拽對象的詳細內容,更多關於JS 面向對象 如何實現封裝拖拽對象的資料請關註WalkonNet其它相關文章!

推薦閱讀: