基础概念的掌握并不是很难,但是要真正的转化为自己的知识,则需要通过大量的实践才行。
拖拽的本质就是让 DOM 元素能够跟着鼠标运动起来。因此,让元素动起来,是我们首先要解决的问题。
设想一下,在我们的页面中仅有一个 .autumn
的 div 标签。它的基本样式如下:
<div class="autumn"></div>
1.autumn {2width: 20px;3height: 20px;4background-color: orange;5}
大家思考一下,当我们希望 .autumn
运动时 (即让它的位置发生变化),可以通过哪些途径?
熟悉 CSS 的同学 5 秒钟就能够想到不少方法,例如改变.autumn
的 margin 值,或者设置 .autumn
的定位方式为relative
,修改其 left/top
属性。又或者直接修改它的 translate
值。
通常情况下不会去修改 margin 值让元素的位置发生改变,以避免对其他元素造成影响。
这里以修改 left
为例,一起来实现一下元素被点击一下就往右移动5个像素的效果。代码如下:
1var autumn = document.querySelector('.autumn');2autumn.style.position = 'relative';34autumn.addEventListener('click', function() {5this.style.left = (this.offsetLeft + 5) + 'px';6}, false);
但是当页面所处的环境支持css3属性 translate
时,我更建议大家在处理元素运动时去修改 translate
的值。因为修改 left/top
可能会导致频繁的重排与回流,而 translate
的属性,则会被作为合成图层,在 GPU 上进行渲染。结果更为流畅。
1.autumn {2transform: translateX(0px);3}
在我们使用 translate
时,不得不面临一个兼容性的问题。
不同浏览器的兼容写法包括如下几种:
['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'OTransform']
因此我们需要判断当前浏览器环境支持的 transform
属性是哪一种,方法如下:
10// 获取当前浏览器支持的transform兼容写法20function getTransform() {30var transform = '',40divStyle = document.createElement('div').style,50_transforms = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'OTransform'],60i = 0,70len = _transforms.length;8090for (; i < len; i++) {10if (_transforms[i] in divStyle) {11// 找到之后立即返回,结束函数12return transform = _transforms[i];13}14}1516// 如果没有找到,就直接返回空字符串17return transform;18}
该方法用于获取当前浏览器支持的 transform
属性。如果返回空字符串,则表示浏览器不支持 transform
,这个时候我们就需要退而求其次选择 left/top
。
为了获取元素的初始位置,我们需要声明一个专门用来获取元素样式的功能函数。获取元素样式在 IE 与其他浏览器中有所不同,因此需要一个兼容性的写法。代码如下:
1function getStyle(elem, property) {2// ie通过currentStyle来获取元素的样式,其他浏览器通过getComputedStyle来获取3return document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(elem, false)[property] : elem.currentStyle[property];4}
有了这个方法之后,我们就可以动手来实现一个获取元素位置的方法了。代码如下:
10function getTargetPos(elem) {20var pos = { x: 0, y: 0 };30var transform = getTransform();40if (transform) {50var transformValue = getStyle(elem, transform);60if (transformValue == 'none') {70elem.style[transform] = 'translate(0, 0)';80return pos;90} else {10var temp = transformValue.match(/-?\d+/g);11return pos = {12x: parseInt(temp[4].trim()),13y: parseInt(temp[5].trim())14}15}16} else {17if (getStyle(elem, 'position') == 'static') {18elem.style.position = 'relative';19return pos;20} else {21var x = parseInt(getStyle(elem, 'left') ? getStyle(elem, 'left') : 0);22var y = parseInt(getStyle(elem, 'top') ? getStyle(elem, 'top') : 0);23return pos = {24x: x,25y: y26}27}28}29}
在拖拽过程中,我们需要不停的设置目标元素的位置,这样他才能够移动起来。因此我们还需要声明一个设置元素位置的方法。
10// pos = { x: 200, y: 100 }20function setTargetPos(elem, pos) {30var transform = getTransform();40if (transform) {50elem.style[transform] = 'translate(' + pos.x + 'px, ' + pos.y + 'px)';60} else {70elem.style.left = pos.x + 'px';80elem.style.top = pos.y + 'px';90}10return elem;11}
有了这几个工具方法,那么我们可以使用更为完善的方式来实现上述要求的效果。代码如下:
1var autumn = document.querySelector('.autumn');23autumn.addEventListener('click', function () {4var curPos = getTargetPos(this);5setTargetPos(this, {6x: curPos.x + 5,7y: curPos.y8});9}, false);
可以结合mousedown, mousemove, mouseup
这三个事件来帮助我们实现拖拽。
mousedown
: 鼠标按下时触发mousemove
: 鼠标移动时触发mouseup
: 鼠标松开时触发我们能够在这些事件触发的回调函数中得到一个事件对象。通过事件对象能够获取到当前鼠标所处的精确位置。鼠标位置信息是实现拖拽的关键。
当鼠标按下(mousedown触发)时,我们需要记住鼠标的初始位置与目标元素的初始位置。当鼠标移动时,目标元素也跟着移动,那么鼠标与目标元素的位置则有如下的关系:
移动后鼠标位置 - 鼠标初始位置 = 移动后目标元素位置 - 目标元素初始位置
如果鼠标位置的差值我们用变量dis
来表示,那么目标元素的位置就等于:
移动后目标元素位置 = dis + 目标元素的初始位置
通过事件对象中提供的鼠标精确位置信息,在鼠标移动时我们可以很轻易的计算出鼠标移动位置的差值。然后根据上面的关系,计算出目标元素的当前位置,这样拖拽就能够实现了。
part1: 准备工作。
10// 获取目标元素对象20var autumn = document.querySelector('.autumn');3040// 声明2个变量用来保存鼠标初始位置的x,y坐标50var startX = 0;60var startY = 0;7080// 声明2个变量用来保存目标元素初始位置的x,y坐标90var sourceX = 0;10var sourceY = 0;
part2: 功能函数
因为之前已经贴过代码,就不再重复
10// 获取当前浏览器支持的transform兼容写法20function getTransform() {}3040// 获取元素属性50function getStyle(elem, property) {}6070// 获取元素的初始位置80function getTargetPos(elem) {}9010// 设置元素的初始位置11function setTargetPos(elem, potions) {}
part3: 声明三个事件的回调
10function move(event) {20// 获取鼠标当前位置30var currentX = event.pageX;40var currentY = event.pageY;5060// 计算差值70var distanceX = currentX - startX;80var distanceY = currentY - startY;9010// 计算并设置元素当前位置11setTargetPos(autumn, {12x: (sourceX + distanceX).toFixed(),13y: (sourceY + distanceY).toFixed()14})15}1617function end(event) {18document.removeEventListener('mousemove', move);19document.removeEventListener('mouseup', end);20// do other things21}
OK,一个简单的拖拽,就这样愉快的实现了。
在前面一章我们学习了面向对象的基础知识。基于这些基础知识我们来将上面实现的拖拽封装为一个拖拽对象。我们的目标是,只要我们声明一个拖拽实例,那么传入的目标元素将自动具备可以被拖拽的功能。
在实际开发中,一个对象我们常常会单独放在一个js文件中,这个js文件将单独作为一个模块,利用各种模块的方式组织起来使用。当然这里没有复杂的模块交互,因为这个例子,我们只需要一个模块即可。
为了避免变量污染,我们需要将模块放置于一个函数自执行方式模拟的块级作用域中。
1;2(function() {3...4})();
在普通的模块组织中,我们只是单纯的将许多js文件压缩成为一个js文件,因此此处的第一个分号则是为了防止上一个模块的结尾不用分号导致报错。必不可少。当然在通过require或者ES6模块等方式就不会出现这样的情况。
我们知道,在封装一个对象的时候,我们可以将属性与方法放置于构造函数或者原型中,而在增加了自执行函数之后,我们又可以将属性和方法防止与模块的内部作用域。这是闭包的知识。
那么我们面临的挑战就在于,如何合理的处理属性与方法的位置。
当然,每一个对象的情况都不一样,不能一概而论,我们需要清晰的知道这三种位置的特性才能做出最适合的决定。
对于方法的判断比较简单。
因为在构造函数中的方法总会在声明一个新的实例时被重复创建,因此我们声明的方法都尽量避免出现在构造函数中。
而如果你的方法中需要用到构造函数中的变量,或者想要公开,那就需要放在原型中。
如果方法需要私有不被外界访问,那么就放置在模块作用域中。
对于属性放置于什么位置有的时候很难做出正确的判断,因此我很难给出一个准确的定义告诉你什么属性一定要放在什么位置,这需要在实际开发中不断的总结经验。但是总的来说,仍然要结合这三个位置的特性来做出最合适的判断。
如果属性值只能被实例单独拥有,比如person对象的name,只能属于某一个person实例,又比如这里拖拽对象中,某一个元素的初始位置,也仅仅只是这个元素的当前位置,这个属性,则适合放在构造函数中。
而如果一个属性仅仅供内部方法访问,这个属性就适合放在模块作用域中。
关于面向对象,上面的几点思考我认为是这篇文章最值得认真思考的精华。如果在封装时没有思考清楚,很可能会遇到很多你意想不到的bug,所以建议大家结合自己的开发经验,多多思考,总结出自己的观点。
根据这些思考,大家可以自己尝试封装一下。然后与我的做一些对比,看看我们的想法有什么不同,在下面例子的注释中,我将自己的想法表达出来。
100;200(function () {300// 这是一个私有属性,不需要被实例访问400var transform = getTransform();500600function Drag(selector) {700// 放在构造函数中的属性,都是属于每一个实例单独拥有800this.elem = typeof selector == 'Object' ? selector : document.getElementById(selector);900this.startX = 0;100this.startY = 0;110this.sourceX = 0;120this.sourceY = 0;130140this.init();150}160170180// 原型190Drag.prototype = {200constructor: Drag,210220init: function () {230// 初始时需要做些什么事情240this.setDrag();250},260270// 稍作改造,仅用于获取当前元素的属性,类似于getName280getStyle: function (property) {290return document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(this.elem, false)[property] : this.elem.currentStyle[property];300},310320// 用来获取当前元素的位置信息,注意与之前的不同之处330getPosition: function () {340var pos = { x: 0, y: 0 };350if (transform) {360var transformValue = this.getStyle(transform);370if (transformValue == 'none') {380this.elem.style[transform] = 'translate(0, 0)';390} else {400var temp = transformValue.match(/-?\d+/g);410pos = {420x: parseInt(temp[4].trim()),430y: parseInt(temp[5].trim())440}450}460} else {470if (this.getStyle('position') == 'static') {480this.elem.style.position = 'relative';490} else {500pos = {510x: parseInt(this.getStyle('left') ? this.getStyle('left') : 0),520y: parseInt(this.getStyle('top') ? this.getStyle('top') : 0)530}540}550}560570return pos;580},590600// 用来设置当前元素的位置610setPostion: function (pos) {620if (transform) {630this.elem.style[transform] = 'translate(' + pos.x + 'px, ' + pos.y + 'px)';640} else {650this.elem.style.left = pos.x + 'px';660this.elem.style.top = pos.y + 'px';670}680},690700// 该方法用来绑定事件710setDrag: function () {720var self = this;730this.elem.addEventListener('mousedown', start, false);740function start(event) {750self.startX = event.pageX;760self.startY = event.pageY;770780var pos = self.getPosition();790800self.sourceX = pos.x;810self.sourceY = pos.y;820830document.addEventListener('mousemove', move, false);840document.addEventListener('mouseup', end, false);850}860870function move(event) {880var currentX = event.pageX;890var currentY = event.pageY;900910var distanceX = currentX - self.startX;920var distanceY = currentY - self.startY;930940self.setPostion({950x: (self.sourceX + distanceX).toFixed(),960y: (self.sourceY + distanceY).toFixed()970})980}990100function end(event) {101document.removeEventListener('mousemove', move);102document.removeEventListener('mouseup', end);103// do other things104}105}106}107108// 私有方法,仅仅用来获取transform的兼容写法109function getTransform() {110var transform = '',111divStyle = document.createElement('div').style,112transformArr = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'OTransform'],113114i = 0,115len = transformArr.length;116117for (; i < len; i++) {118if (transformArr[i] in divStyle) {119return transform = transformArr[i];120}121}122123return transform;124}125126// 一种对外暴露的方式127window.Drag = Drag;128})();129130// 使用:声明2个拖拽实例131new Drag('target');132new Drag('target2');
这样一个拖拽对象就封装完毕了。
建议大家根据我提供的思维方式,多多尝试封装一些组件。比如封装一个弹窗,封装一个循环轮播等。练得多了,面向对象就不再是问题了。这种思维方式,在未来任何时候都是能够用到的。
在前面的学习我们已经知道了可以使用$.extend
扩展jquery工具方法,使用$.fn.extend
扩展原型方法。当然,这里的拖拽插件扩展为原型方法是最合适的。
1// 通过扩展方法将拖拽扩展为jQuery的一个实例方法2(function ($) {3$.fn.extend({4canDrag: function () {5new Drag(this[0]);6return this; // 注意:为了保证jQuery所有的方法都能够链式访问,每一个方法的最后都需要返回this,即返回jQuery实例7}8})9})(jQuery);
这样我们就能够很轻松的让目标DOM元素具备拖拽能力。
$('target').canDrag();