美文网首页JavaScript码无界前端WEB开发秘籍
前端基础进阶(十):面向对象实战之封装拖拽对象

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

作者: 这波能反杀 | 来源:发表于2017-03-15 00:56 被阅读10122次
    终于

    前面几篇文章,我跟大家分享了JavaScript的一些基础知识,这篇文章,将会进入第一个实战环节:利用前面几章的所涉及到的知识,封装一个拖拽对象。为了能够帮助大家了解更多的方式与进行对比,我会使用三种不同的方式来实现拖拽。

    • 不封装对象直接实现;
    • 利用原生JavaScript封装拖拽对象;
    • 通过扩展jQuery来实现拖拽对象。

    本文的例子会放置于codepen.io中,供大家在阅读时直接查看。如果对于codepen不了解的同学,可以花点时间稍微了解一下。

    拖拽的实现过程会涉及到非常多的实用小知识,因此为了巩固我自己的知识积累,也为了大家能够学到更多的知识,我会尽量详细的将一些细节分享出来,相信大家认真阅读之后,一定能学到一些东西。

    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');
    

    这样一个拖拽对象就封装完毕了。

    建议大家根据我提供的思维方式,多多尝试封装一些组件。比如封装一个弹窗,封装一个循环轮播等。练得多了,面向对象就不再是问题了。这种思维方式,在未来任何时候都是能够用到的。

    下一章分析jQuery对象的实现,与如何将我们这里封装的拖拽对象扩展为jQuery插件。

    相关文章

      网友评论

      • 2aa405c6cb40:照着敲了一遍,好想睡觉。。。然后还出错了,检查一遍才好,有好的学习建议吗?
        8b60672f77e8:"照着敲了一遍,好想睡觉" 是不是 对于代码的理解 还是一知半解 ? 如果是的话,说明的你基础还比较弱需要多找一些基础知识的书来看看(比如高程或者js设计模式的书) 建议这部分内容你就先跳过了 先看其他容易理解的地方
      • 終愛妳壹生:请问拖拽那个脑图,是用什么软件做的
      • mWyjye:不用正则的话,用slice(7,-1).split(',')更简单吧。
      • 7a82589dca7c:function Drag(id){
        var _this=this;
        this.elem=document.getElementById(id);
        this.mouseX=0;
        this.mouseY=0;
        this.targetX=0;
        this.targetY=0;
        this.elem.addEventListener('mousedown',function(){
        _this.start();
        },false);
        }
        Drag.prototype={
        constructor:Drag,
        start:function (ev){
        var ev=ev||event;
        var _this=this;
        this.mouseX=ev.clientX;
        this.mouseY=ev.clientY;
        var pos=this.getTargetPos();
        this.targetX=pos.x;
        this.targetY=pos.y;
        document.addEventListener('mousemove',function(){
        _this.move();
        },false);
        document.addEventListener('mouseup',function(){
        _this.end();
        },false);
        },
        move:function(ev){
        var ev=ev||event;
        var currentX=ev.clientX;
        var currentY=ev.clientY;
        var disX=currentX-this.mouseX;
        var disY=currentY-this.mouseY;
        this.setTargetPos({x:(disX+this.targetX).toFixed(),y:(disY+this.targetY).toFixed()});

        },
        end:function(ev){
        var ev=ev||event;
        var _this=this;
        document.removeEventListener('mousemove',function(){ _this.move();});
        document.removeEventListener('mouseup',function(){ _this.end();});
        /* document.onmousemove=null;
        document.onmouseup=null;*/
        },
        getStyle:function (attr){
        return (this.elem.currentStyle||getComputedStyle(this.elem,null))[attr];
        },
        getTargetPos:function (){
        ..........
        },
        setTargetPos:function (pos){
        ..........
        }
        }
        function getTransform(){
        ......
        }

        new Drag('div1');
        new Drag('div2');

        波神,你好,我这样写的,当鼠标(onmouseup)事件时,物体不能停下来,能看看什么问题吗?已跪了两天了,谢谢(其中的 getTargetPos、getTransform和 setTargetPos和你写的一样)
      • 马达加斯加的北极熊:感觉学到了好多哦~:stuck_out_tongue_closed_eyes:
      • 一吻江山:“我们又可以将属性和方法防止与模块的内部作用域”这里的“防止与”是错别字,“放置于” @波同学
      • 3bc18dc4aeb4:波同学,绑定监听事件的回调函数(start, move, end)可以写到原型外面吗?我试了下但event对象传不过去,这是为什么?
        b7bfb8ee19a4:start, move, end中都是需要访问Drag对象的中的self.startX的,如果放到原型外面,时访问不到的。event应该能访问到
        这波能反杀:@捡月 你仔细想想写在原型外面代表了什么,结合闭包理解一下,这些我应该在文章里说过的
      • 好想要挥霍_85d7:this.elem = typeof selector == 'Object' ? selector : document.getElementById(selector);
        中应该是'object'吧,否则传入dom对象无法实现拖拽
        这波能反杀:@好想要挥霍_85d7 en
      • cb1beb9ac254:你在匹配transform的值时,那个trim()方法是不是有点多余,parseInt()好像是可以过滤掉字符串的前后空格的,还请大神指教
        这波能反杀:@iFuhang 你可以试试不绑定在document上会出现什么问题就知道了
        cb1beb9ac254:@波同学 嗯呢,请问一下最后那个监听事件为什么要绑定在document上呢
        这波能反杀:@iFuhang 一种习惯问题
      • 三生三世小世界:codepen注册不了怎么办
      • 128f587d6ed6:学习了!
      • 徵羽和雪樱:波神,虽然我没发现我下午提的问题的答案,但是我有一个新问题,就是这个拖放start事件里,我觉得得添加event.preventDefault();不然会有一些问题吧?而且后面的监听事件里,个人觉得最好是把监听元素换为当前元素,这样比较合逻辑些,您觉得呢?
        这波能反杀:@徵羽和雪樱 你可以自己动手试试监听当前元素会出现什么问题,至于加不加取消默认,这是根据需求来定的
      • 徵羽和雪樱: Drag.prototype={

        start : function(e){
        this.startX = e.pageX;
        this.startY = e.pageY;

        var pos = this.getTargetPos();
        this.sourceX = pos.x;
        this.sourceY = pos.y;

        this.elem.addEventListener('mousemove' , this.move , false);
        this.elem.addEventListener('mouseup' , this.end , false);
        },
        move : function(e){
        var currentX = e.pageX,
        currentY = e.pageY;

        var distanceX = currentX - this.startX,
        distanceY = currentY - this.startY;

        setTargetPos(this.elem,{
        x : (this.sourceX + distanceX).tofixed(),
        y : (this.sourceY + distanceY).tofixed()
        })
        },
        end : function(){
        this.elem.removeEventListener('mousemove',this.move);
        this.elem.removeEventListener('mouseup',this.end);
        },
        init : function(){
        this.elem.addEventListener('mousedown' , this.start , false);
        }
        }
        波神,我把对象里的事件写成这样子,为什么不行呢?this.getTargetPos()执行的时候说方法不存在,
        这波能反杀:@徵羽和雪樱 你和我的源码对比看看吧
        这波能反杀:@徵羽和雪樱 你这方法没定义
        徵羽和雪樱:我截取了对象的一部分,getTargetPos()有在对象里定义的
      • 29f26f9f36ce:一天看完10篇,感觉作者是第二个阮一峰,精辟入里,第一次看博文这么感动,好想和作者做朋友,让我前端学习道路不再迷茫
      • 744bb97e6c1a:请问一下构造函数中四个位置属性干嘛用的?我删掉它之后感觉没什么影响:joy:
        5c7650875e94:应该是在初始化后,在setDrag里都被重新定义了吧
      • 邪恶的罐子:问个题外话...你这个思维导图用的什么软件做的:smirk:蛮漂亮
        邪恶的罐子:@波同学 谢谢:pray:
        这波能反杀:@邪恶的罐子 mac的mindnode
      • small_a:试了一下你的demo,有个bug,当元素被拖拽到初始位置的上面之后,就无法拖拽了,。。原因应该是因为正则的时候没有把translate的负号匹配到吧。。。。。
        small_a: @波同学 ...我这个还得改一下,现在的正则没有考虑小数点,我在移动端运行的时候,transform会产生小数点,所以会导致出错,正则中加上对小数点的判断就可以了/-?[0-9\.]+/g
        这波能反杀:@small_a 赞
        small_a:可以通过修改正则来完善的。。。你的正则用的有点不太好。。。我自己改了一版。。
        var reg = /-?\d+/g;
        pos.x = Number(style.match(reg)[4]);
        pos.y = Number(style.match(reg)[5]);
        这样匹配出来的不需要split等处理。。。。同时-号也可以正确匹配。。。。
        一点小意见。。。
        事实上就是因为这个问题。。导致你的demo拖拽到初始位置左侧,初始位置上侧,都会出现无法拖拽的情况,就是因为正则没有把transform中的-号匹配出来
      • 97900e44363d:66666666666666666666
      • kerush:我看到了前进的方向:+1:
      • 都增刚:获取样式的函数,这个也挺好用的
        function getStyle(obj,name) {
        return (obj.currentStyle || getComputedStyle(obj,false))[name];
        }
      • chenpipy:更新了,支持支持😀
      • a2934a679386:正则还是应该给它声明一个变量,给予注释作用,不然过一段时间回来看,一脸懵逼。简单的还好,复杂一点呢?
      • 董小姐Stly:感谢波同学🙏🙏🙏
      • 一本万利:大力支持
      • e08c9f696bbf:提个问题:
        setDrag方法内你定义了self = this,
        后面所有参数你都没有使用,你提供的demo中可以使用没问题。
        我自己使用的时候必须用self.startX才可正常使用
        这波能反杀:@imba_fff 我的问题,忘了:joy:
      • 2c4553b501e9:不过现在已经不流行拖拽了
        e08c9f696bbf:授人以鱼不如授人以渔
      • dd97a851dcd4:点赞占沙发献朵花:cherry_blossom::cherry_blossom::cherry_blossom:
      • aaa790849e4b:波同学,受益匪浅,感谢!
      • 2ea9af586180:继续努力
      • HelloKang:前排好,顶,赞

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

      本文链接:https://www.haomeiwen.com/subject/mtwogttx.html