詳解JavaScript面向?qū)ο髮?shí)戰(zhàn)之封裝拖拽對(duì)象
概述
為了能夠幫助大家了解更多的方式與進(jìn)行對(duì)比,我會(huì)使用三種不同的方式來(lái)實(shí)現(xiàn)拖拽。
- 不封裝對(duì)象直接實(shí)現(xiàn);
- 利用原生JavaScript封裝拖拽對(duì)象;
- 通過(guò)擴(kuò)展jQuery來(lái)實(shí)現(xiàn)拖拽對(duì)象。
拖拽的實(shí)現(xiàn)過(guò)程會(huì)涉及到非常多的實(shí)用小知識(shí),因此為了鞏固我自己的知識(shí)積累,也為了大家能夠?qū)W到更多的知識(shí),我會(huì)盡量詳細(xì)的將一些細(xì)節(jié)分享出來(lái),相信大家認(rèn)真閱讀之后,一定能學(xué)到一些東西。
1、如何讓一個(gè)DOM元素動(dòng)起來(lái)
我們常常會(huì)通過(guò)修改元素的top,left,translate來(lái)其的位置發(fā)生改變。在下面的例子中,每點(diǎn)擊一次按鈕,對(duì)應(yīng)的元素就會(huì)移動(dòng)5px。大家可點(diǎn)擊查看。
點(diǎn)擊查看一個(gè)讓元素動(dòng)起來(lái)的小例子
由于修改一個(gè)元素top/left值會(huì)引起頁(yè)面重繪,而translate不會(huì),因此從性能優(yōu)化上來(lái)判斷,我們會(huì)優(yōu)先使用translate屬性。
2、如何獲取當(dāng)前瀏覽器支持的transform兼容寫(xiě)法
transform是css3的屬性,當(dāng)我們使用它時(shí)就不得不面對(duì)兼容性的問(wèn)題。不同版本瀏覽器的兼容寫(xiě)法大致有如下幾種:
['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'OTransform']
因此我們需要判斷當(dāng)前瀏覽器環(huán)境支持的transform屬性是哪一種,方法如下:
// 獲取當(dāng)前瀏覽器支持的transform兼容寫(xiě)法 function getTransform() { var transform = '', divStyle = document.createElement('div').style, // 可能涉及到的幾種兼容性寫(xiě)法,通過(guò)循環(huán)找出瀏覽器識(shí)別的那一個(gè) transformArr = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'OTransform'], i = 0, len = transformArr.length; for(; i < len; i++) { if(transformArr[i] in divStyle) { // 找到之后立即返回,結(jié)束函數(shù) return transform = transformArr[i]; } } // 如果沒(méi)有找到,就直接返回空字符串 return transform; }
該方法用于獲取瀏覽器支持的transform屬性。如果返回的為空字符串,則表示當(dāng)前瀏覽器并不支持transform,這個(gè)時(shí)候我們就需要使用left,top值來(lái)改變?cè)氐奈恢谩H绻С?,就改變transform的值。
3、如何獲取元素的初始位置
我們首先需要獲取到目標(biāo)元素的初始位置,因此這里我們需要一個(gè)專(zhuān)門(mén)用來(lái)獲取元素樣式的功能函數(shù)。
但是獲取元素樣式在IE瀏覽器與其他瀏覽器有一些不同,因此我們需要一個(gè)兼容性的寫(xiě)法。
function getStyle(elem, property) { // ie通過(guò)currentStyle來(lái)獲取元素的樣式,其他瀏覽器通過(guò)getComputedStyle來(lái)獲取 return document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(elem, false)[property] : elem.currentStyle[property]; }
有了這個(gè)方法之后,就可以開(kāi)始動(dòng)手寫(xiě)獲取目標(biāo)元素初始位置的方法了。
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 } } } }
在拖拽過(guò)程中,我們需要不停的設(shè)置目標(biāo)元素的新位置,這樣它才會(huì)移動(dòng)起來(lái),因此我們需要一個(gè)設(shè)置目標(biāo)元素位置的方法。
// 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上的瀏覽器中,結(jié)合mousedown、mousemove、mouseup這三個(gè)事件可以幫助我們實(shí)現(xiàn)拖拽。
- mousedown鼠標(biāo)按下時(shí)觸發(fā)
- mousemove鼠標(biāo)按下后拖動(dòng)時(shí)觸發(fā)
- mouseup鼠標(biāo)松開(kāi)時(shí)觸發(fā)
而在移動(dòng)端,分別與之對(duì)應(yīng)的則是touchstart、touchmove、touchend。
當(dāng)我們將元素綁定這些事件時(shí),有一個(gè)事件對(duì)象將會(huì)作為參數(shù)傳遞給回調(diào)函數(shù),通過(guò)事件對(duì)象,我們可以獲取到當(dāng)前鼠標(biāo)的精確位置,鼠標(biāo)位置信息是實(shí)現(xiàn)拖拽的關(guān)鍵。
事件對(duì)象十分重要,其中包含了非常多的有用的信息,這里我就不擴(kuò)展了,大家可以在函數(shù)中將事件對(duì)象打印出來(lái)查看其中的具體屬性,這個(gè)方法對(duì)于記不清事件對(duì)象重要屬性的童鞋非常有用。
6、拖拽的原理
當(dāng)事件觸發(fā)時(shí),我們可以通過(guò)事件對(duì)象獲取到鼠標(biāo)的精切位置。這是實(shí)現(xiàn)拖拽的關(guān)鍵。當(dāng)鼠標(biāo)按下(mousedown觸發(fā))時(shí),我們需要記住鼠標(biāo)的初始位置與目標(biāo)元素的初始位置,我們的目標(biāo)就是實(shí)現(xiàn)當(dāng)鼠標(biāo)移動(dòng)時(shí),目標(biāo)元素也跟著移動(dòng),根據(jù)常理我們可以得出如下關(guān)系:
移動(dòng)后的鼠標(biāo)位置 - 鼠標(biāo)初始位置 = 移動(dòng)后的目標(biāo)元素位置 - 目標(biāo)元素的初始位置
如果鼠標(biāo)位置的差值我們用dis來(lái)表示,那么目標(biāo)元素的位置就等于:
移動(dòng)后目標(biāo)元素的位置 = dis + 目標(biāo)元素的初始位置
通過(guò)事件對(duì)象,我們可以精確的知道鼠標(biāo)的當(dāng)前位置,因此當(dāng)鼠標(biāo)拖動(dòng)(mousemove)時(shí),我們可以不停的計(jì)算出鼠標(biāo)移動(dòng)的差值,以此來(lái)求出目標(biāo)元素的當(dāng)前位置。這個(gè)過(guò)程,就實(shí)現(xiàn)了拖拽。
而在鼠標(biāo)松開(kāi)(mouseup)結(jié)束拖拽時(shí),我們需要處理一些收尾工作。詳情見(jiàn)代碼。
7、 我又來(lái)推薦思維導(dǎo)圖輔助寫(xiě)代碼了
常常有新人朋友跑來(lái)問(wèn)我,如果邏輯思維能力不強(qiáng),能不能寫(xiě)代碼做前端。我的答案是:能。因?yàn)榻柚季S導(dǎo)圖,可以很輕松的彌補(bǔ)邏輯的短板。而且比在自己頭腦中腦補(bǔ)邏輯更加清晰明了,不易出錯(cuò)。
上面第六點(diǎn)我介紹了原理,因此如何做就顯得不是那么難了,而具體的步驟,則在下面的思維導(dǎo)圖中明確給出,我們只需要按照這個(gè)步驟來(lái)寫(xiě)代碼即可,試試看,一定很輕松。
使用思維導(dǎo)圖清晰的表達(dá)出整個(gè)拖拽過(guò)程我們需要干的事情
8、代碼實(shí)現(xiàn)
part1、準(zhǔn)備工作
// 獲取目標(biāo)元素對(duì)象 var oElem = document.getElementById('target'); // 聲明2個(gè)變量用來(lái)保存鼠標(biāo)初始位置的x,y坐標(biāo) var startX = 0; var startY = 0; // 聲明2個(gè)變量用來(lái)保存目標(biāo)元素初始位置的x,y坐標(biāo) var sourceX = 0; var sourceY = 0;
part2、功能函數(shù)
因?yàn)橹耙呀?jīng)貼過(guò)代碼,就不再重復(fù)
// 獲取當(dāng)前瀏覽器支持的transform兼容寫(xiě)法 function getTransform() {} // 獲取元素屬性 function getStyle(elem, property) {} // 獲取元素的初始位置 function getTargetPos(elem) {} // 設(shè)置元素的初始位置 function setTargetPos(elem, potions) {}
part3、聲明三個(gè)事件的回調(diào)函數(shù)
這三個(gè)方法就是實(shí)現(xiàn)拖拽的核心所在,我將嚴(yán)格按照上面思維導(dǎo)圖中的步驟來(lái)完成我們的代碼。
// 綁定在mousedown上的回調(diào),event為傳入的事件對(duì)象 function start(event) { // 獲取鼠標(biāo)初始位置 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) { // 獲取鼠標(biāo)當(dāng)前位置 var currentX = event.pageX; var currentY = event.pageY; // 計(jì)算差值 var distanceX = currentX - startX; var distanceY = currentY - startY; // 計(jì)算并設(shè)置元素當(dāng)前位置 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,一個(gè)簡(jiǎn)單的拖拽,就這樣愉快的實(shí)現(xiàn)了。點(diǎn)擊下面的鏈接,可以在線(xiàn)查看該例子的demo。
9、封裝拖拽對(duì)象
我們來(lái)將上面實(shí)現(xiàn)的拖拽封裝為一個(gè)拖拽對(duì)象。我們的目標(biāo)是,只要我們聲明一個(gè)拖拽實(shí)例,那么傳入的目標(biāo)元素將自動(dòng)具備可以被拖拽的功能。
在實(shí)際開(kāi)發(fā)中,一個(gè)對(duì)象我們常常會(huì)單獨(dú)放在一個(gè)js文件中,這個(gè)js文件將單獨(dú)作為一個(gè)模塊,利用各種模塊的方式組織起來(lái)使用。當(dāng)然這里沒(méi)有復(fù)雜的模塊交互,因?yàn)檫@個(gè)例子,我們只需要一個(gè)模塊即可。
為了避免變量污染,我們需要將模塊放置于一個(gè)函數(shù)自執(zhí)行方式模擬的塊級(jí)作用域中。
(function() { ... })();
在普通的模塊組織中,我們只是單純的將許多js文件壓縮成為一個(gè)js文件,因此此處的第一個(gè)分號(hào)則是為了防止上一個(gè)模塊的結(jié)尾不用分號(hào)導(dǎo)致報(bào)錯(cuò)。必不可少。當(dāng)然在通過(guò)require或者ES6模塊等方式就不會(huì)出現(xiàn)這樣的情況。
我們知道,在封裝一個(gè)對(duì)象的時(shí)候,我們可以將屬性與方法放置于構(gòu)造函數(shù)或者原型中,而在增加了自執(zhí)行函數(shù)之后,我們又可以將屬性和方法防止與模塊的內(nèi)部作用域。這是閉包的知識(shí)。
那么我們面臨的挑戰(zhàn)就在于,如何合理的處理屬性與方法的位置。
當(dāng)然,每一個(gè)對(duì)象的情況都不一樣,不能一概而論,我們需要清晰的知道這三種位置的特性才能做出最適合的決定。
- 構(gòu)造函數(shù)中: 屬性與方法為當(dāng)前實(shí)例單獨(dú)擁有,只能被當(dāng)前實(shí)例訪(fǎng)問(wèn),并且每聲明一個(gè)實(shí)例,其中的方法都會(huì)被重新創(chuàng)建一次。
- 原型中: 屬性與方法為所有實(shí)例共同擁有,可以被所有實(shí)例訪(fǎng)問(wèn),新聲明實(shí)例不會(huì)重復(fù)創(chuàng)建方法。
- 模塊作用域中:屬性和方法不能被任何實(shí)例訪(fǎng)問(wèn),但是能被內(nèi)部方法訪(fǎng)問(wèn),新聲明的實(shí)例,不會(huì)重復(fù)創(chuàng)建相同的方法。
對(duì)于方法的判斷比較簡(jiǎn)單。
因?yàn)樵跇?gòu)造函數(shù)中的方法總會(huì)在聲明一個(gè)新的實(shí)例時(shí)被重復(fù)創(chuàng)建,因此我們聲明的方法都盡量避免出現(xiàn)在構(gòu)造函數(shù)中。
而如果你的方法中需要用到構(gòu)造函數(shù)中的變量,或者想要公開(kāi),那就需要放在原型中。
如果方法需要私有不被外界訪(fǎng)問(wèn),那么就放置在模塊作用域中。
對(duì)于屬性放置于什么位置有的時(shí)候很難做出正確的判斷,因此我很難給出一個(gè)準(zhǔn)確的定義告訴你什么屬性一定要放在什么位置,這需要在實(shí)際開(kāi)發(fā)中不斷的總結(jié)經(jīng)驗(yàn)。但是總的來(lái)說(shuō),仍然要結(jié)合這三個(gè)位置的特性來(lái)做出最合適的判斷。
如果屬性值只能被實(shí)例單獨(dú)擁有,比如person對(duì)象的name,只能屬于某一個(gè)person實(shí)例,又比如這里拖拽對(duì)象中,某一個(gè)元素的初始位置,也僅僅只是這個(gè)元素的當(dāng)前位置,這個(gè)屬性,則適合放在構(gòu)造函數(shù)中。
而如果一個(gè)屬性?xún)H僅供內(nèi)部方法訪(fǎng)問(wèn),這個(gè)屬性就適合放在模塊作用域中。
關(guān)于面向?qū)ο?,上面的幾點(diǎn)思考我認(rèn)為是這篇文章最值得認(rèn)真思考的精華。如果在封裝時(shí)沒(méi)有思考清楚,很可能會(huì)遇到很多你意想不到的bug,所以建議大家結(jié)合自己的開(kāi)發(fā)經(jīng)驗(yàn),多多思考,總結(jié)出自己的觀(guān)點(diǎn)。
根據(jù)這些思考,大家可以自己嘗試封裝一下。然后與我的做一些對(duì)比,看看我們的想法有什么不同,在下面例子的注釋中,我將自己的想法表達(dá)出來(lái)。
js 源碼
(function() { // 這是一個(gè)私有屬性,不需要被實(shí)例訪(fǎng)問(wèn) var transform = getTransform(); function Drag(selector) { // 放在構(gòu)造函數(shù)中的屬性,都是屬于每一個(gè)實(shí)例單獨(dú)擁有 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() { // 初始時(shí)需要做些什么事情 this.setDrag(); }, // 稍作改造,僅用于獲取當(dāng)前元素的屬性,類(lèi)似于getName getStyle: function(property) { return document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(this.elem, false)[property] : this.elem.currentStyle[property]; }, // 用來(lái)獲取當(dāng)前元素的位置信息,注意與之前的不同之處 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; }, // 用來(lái)設(shè)置當(dāng)前元素的位置 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'; } }, // 該方法用來(lái)綁定事件 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 } } } // 私有方法,僅僅用來(lái)獲取transform的兼容寫(xiě)法 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; } // 一種對(duì)外暴露的方式 window.Drag = Drag; })(); // 使用:聲明2個(gè)拖拽實(shí)例 new Drag('target'); new Drag('target2');
這樣一個(gè)拖拽對(duì)象就封裝完畢了。
建議大家根據(jù)我提供的思維方式,多多嘗試封裝一些組件。比如封裝一個(gè)彈窗,封裝一個(gè)循環(huán)輪播等。練得多了,面向?qū)ο缶筒辉偈菃?wèn)題了。這種思維方式,在未來(lái)任何時(shí)候都是能夠用到的。
以上就是詳解JavaScript面向?qū)ο髮?shí)戰(zhàn)之封裝拖拽對(duì)象的詳細(xì)內(nèi)容,更多關(guān)于JS 面向?qū)ο?如何實(shí)現(xiàn)封裝拖拽對(duì)象的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript兩種function的定義介紹及區(qū)別說(shuō)明
javascript兩種function的定義方式function a(){}和a=function(){}具體使用如下,感興趣的朋友可以參考下,希望對(duì)你對(duì)你學(xué)習(xí)function的定義有所幫助2013-05-05js實(shí)現(xiàn)倒計(jì)時(shí)器自定義時(shí)間和暫停
這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)倒計(jì)時(shí)器自定義時(shí)間和暫停,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02bootstrapValidator表單驗(yàn)證插件學(xué)習(xí)
這篇文章主要為大家詳細(xì)介紹了表單驗(yàn)證插件bootstrapValidator的使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12百度坐標(biāo)(BD09)、國(guó)測(cè)局坐標(biāo)(火星坐標(biāo),GCJ02)、和WGS84坐標(biāo)系之間的轉(zhuǎn)換
這篇文章主要介紹了百度坐標(biāo)(BD09)、國(guó)測(cè)局坐標(biāo)(火星坐標(biāo),GCJ02)、和WGS84坐標(biāo)系之間的轉(zhuǎn)換的相關(guān)資料,需要的朋友可以參考下2016-02-02JavaScript使用DeviceOne開(kāi)發(fā)實(shí)戰(zhàn)(二) 生成調(diào)試安裝包
這篇文章主要介紹了JavaScript使用DeviceOne開(kāi)發(fā)實(shí)戰(zhàn)(二) 生成調(diào)試安裝包的相關(guān)資料,需要的朋友可以參考下2015-12-12javaScript實(shí)現(xiàn)復(fù)選框全選反選事件詳解
這篇文章主要為大家詳細(xì)介紹了javaScript實(shí)現(xiàn)復(fù)選框全選反選事件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09