前端基础进阶(十):面向对象实战之封装拖拽对象

终于

前面几篇文章,我跟你们分享了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

相关文章
相关标签/搜索