前面幾篇文章,我跟大家分享了JavaScript的一些基礎知識,這篇文章,將會進入第一個實戰環節:利用前面幾章的所涉及到的知識,封裝一個拖曳物件。為了能夠幫助大家了解更多的方式與進行對比,我會使用三種不同的方式來實現拖曳。
不封裝物件直接實作;
使用原生JavaScript封裝拖曳物件;
透過擴充jQuery來實作拖曳物件。
本文的範例會放置在codepen.io中,供大家在閱讀時直接查看。如果對於codepen不了解的同學,可以花點時間稍微了解一下。
拖曳的實現過程會涉及到非常多的實用小知識,因此為了鞏固我自己的知識積累,也為了大家能夠學到更多的知識,我會盡量詳細的將一些細節分享出來,相信大家認真閱讀之後,一定能學到一些東西。
我們常常會透過修改元素的<a href="http://www.php.cn/wiki/904.html" target="_blank">top</a>,left,translate
來其的位置發生改變。在下面的範例中,每點擊一次按鈕,對應的元素就會移動5px。大家可點選查看。
點擊查看一個讓元素動起來的小例子
由於修改一個元素top/left值會引起頁面重繪,而translate不會,因此從性能優化上來判斷,我們會優先使用translate屬性。
transform是css3的屬性,當我們使用它時就不得不面對相容性的問題。不同版本瀏覽器的相容寫法大致有以下幾種:
['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'OTransform']
#因此我們需要判斷目前瀏覽器環境支援的transform屬性是哪一種,方法如下:
// 获取当前浏览器支持的transform兼容写法 function getTransform() { var transform = '', pStyle = document.createElement('p').style, // 可能涉及到的几种兼容性写法,通过循环找出浏览器识别的那一个 transformArr = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'OTransform'], i = 0, len = transformArr.length; for(; i < len; i++) { if(transformArr[i] in pStyle) { // 找到之后立即返回,结束函数 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、touch<a href="http://www.php.cn/wiki/1048.html" target="_blank">end</a>
。
當我們將元素綁定這些事件時,有一個事件對象將會作為參數傳遞給回呼函數,透過事件對象,我們可以取得到目前滑鼠的精確位置,滑鼠位置資訊是實現拖曳的關鍵。
事件物件十分重要,其中包含了非常多的有用的信息,這裡我就不擴展了,大家可以在函數中將事件對像打印出來查看其中的具體屬性,這個方法對於記不清事件物件重要屬性的童鞋非常有用。
当事件触发时,我们可以通过事件对象获取到鼠标的精切位置。这是实现拖拽的关键。当鼠标按下(mousedown触发)时,我们需要记住鼠标的初始位置与目标元素的初始位置,我们的目标就是实现当鼠标移动时,目标元素也跟着移动,根据常理我们可以得出如下关系:
移动后的鼠标位置 - 鼠标初始位置 = 移动后的目标元素位置 - 目标元素的初始位置
如果鼠标位置的差值我们用dis来表示,那么目标元素的位置就等于:
移动后目标元素的位置 = dis + 目标元素的初始位置
通过事件对象,我们可以精确的知道鼠标的当前位置,因此当鼠标拖动(mousemove)时,我们可以不停的计算出鼠标移动的差值,以此来求出目标元素的当前位置。这个过程,就实现了拖拽。
而在鼠标松开(mouseup)结束拖拽时,我们需要处理一些收尾工作。详情见代码。
常常有新人朋友跑来问我,如果逻辑思维能力不强,能不能写代码做前端。我的答案是:能。因为借助思维导图,可以很轻松的弥补逻辑的短板。而且比在自己头脑中脑补逻辑更加清晰明了,不易出错。
上面第六点我介绍了原理,因此如何做就显得不是那么难了,而具体的步骤,则在下面的思维导图中明确给出,我们只需要按照这个步骤来写代码即可,试试看,一定很轻松。
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实现拖拽
在前面一章我给大家分享了面向对象如何实现,基于那些基础知识,我们来将上面实现的拖拽封装为一个拖拽对象。我们的目标是,只要我们声明一个拖拽实例,那么传入的目标元素将自动具备可以被拖拽的功能。
在实际开发中,一个对象我们常常会单独放在一个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 = '', pStyle = document.createElement('p').style, transformArr = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'OTransform'], i = 0, len = transformArr.length; for(; i < len; i++) { if(transformArr[i] in pStyle) { return transform = transformArr[i]; } } return transform; } // 一种对外暴露的方式 window.Drag = Drag; })(); // 使用:声明2个拖拽实例 new Drag('target'); new Drag('target2');
这样一个拖拽对象就封装完毕了。
建议大家根据我提供的思维方式,多多尝试封装一些组件。比如封装一个弹窗,封装一个循环轮播等。练得多了,面向对象就不再是问题了。这种思维方式,在未来任何时候都是能够用到的。
以上是前端進階(十):封裝拖曳對象的詳細內容。更多資訊請關注PHP中文網其他相關文章!