摘要:前面幾篇文章,我跟大家分享了的一些基礎知識,這篇文章,將會進入第一個實戰環節利用前面幾章的所涉及到的知識,封裝一個拖拽對象。不封裝對象直接實現利用原生封裝拖拽對象通過擴展來實現拖拽對象。
前面幾篇文章,我跟大家分享了JavaScript的一些基礎知識,這篇文章,將會進入第一個實戰環節:利用前面幾章的所涉及到的知識,封裝一個拖拽對象。為了能夠幫助大家了解更多的方式與進行對比,我會使用三種不同的方式來實現拖拽。
不封裝對象直接實現;
利用原生JavaScript封裝拖拽對象;
通過擴展jQuery來實現拖拽對象。
本文的例子會放置于codepen.io中,供大家在閱讀時直接查看。如果對于codepen不了解的同學,可以花點時間稍微了解一下。
拖拽的實現過程會涉及到非常多的實用小知識,因此為了鞏固我自己的知識積累,也為了大家能夠學到更多的知識,我會盡量詳細的將一些細節分享出來,相信大家認真閱讀之后,一定能學到一些東西。
我們常常會通過修改元素的top,left,translate來其的位置發生改變。在下面的例子中,每點擊一次按鈕,對應的元素就會移動5px。大家可點擊查看。
點擊查看一個讓元素動起來的小例子
由于修改一個元素top/left值會引起頁面重繪,而translate不會,因此從性能優化上來判斷,我們會優先使用translate屬性。
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的值。
我們首先需要獲取到目標元素的初始位置,因此這里我們需要一個專門用來獲取元素樣式的功能函數。
但是獲取元素樣式在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)結束拖拽時,我們需要處理一些收尾工作。詳情見代碼。
常常有新人朋友跑來問我,如果邏輯思維能力不強,能不能寫代碼做前端。我的答案是:能。因為借助思維導圖,可以很輕松的彌補邏輯的短板。而且比在自己頭腦中腦補邏輯更加清晰明了,不易出錯。
上面第六點我介紹了原理,因此如何做就顯得不是那么難了,而具體的步驟,則在下面的思維導圖中明確給出,我們只需要按照這個步驟來寫代碼即可,試試看,一定很輕松。
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 = "", 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插件。
前端基礎進階系列目錄
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/90546.html
摘要:不過其實簡書文章評論里有很多大家的問題以及解答,對于進一步理解文中知識幫助很大的,算是有點可惜吧。不過也希望能夠對正在學習前端的你有一些小幫助。如果在閱讀中發現了一些錯誤,請在評論里告訴我,我會及時更改。 前端基礎進階(一):內存空間詳細圖解 前端基礎進階(二):執行上下文詳細圖解 前端基礎進階(三):變量對象詳解 前端基礎進階(四):詳細圖解作用域鏈與閉包 前端基礎進階(五):全方位...
摘要:面向對象三大特征繼承性多態性封裝性接口。第五階段封裝一個屬于自己的框架框架封裝基礎事件流冒泡捕獲事件對象事件框架選擇框架。核心模塊和對象全局對象,,,事件驅動,事件發射器加密解密,路徑操作,序列化和反序列化文件流操作服務端與客戶端。 第一階段: HTML+CSS:HTML進階、CSS進階、div+css布局、HTML+css整站開發、 JavaScript基礎:Js基礎教程、js內置對...
摘要:面向對象三大特征繼承性多態性封裝性接口。第五階段封裝一個屬于自己的框架框架封裝基礎事件流冒泡捕獲事件對象事件框架選擇框架。核心模塊和對象全局對象,,,事件驅動,事件發射器加密解密,路徑操作,序列化和反序列化文件流操作服務端與客戶端。 第一階段: HTML+CSS:HTML進階、CSS進階、div+css布局、HTML+css整站開發、 JavaScript基礎:Js基礎教程、js內置對...
摘要:跨域請求詳解從繁至簡前端掘金什么是為什么要用是的一種使用模式,可用于解決主流瀏覽器的跨域數據訪問的問題。異步編程入門道典型的面試題前端掘金在界中,開發人員的需求量一直居高不下。 jsonp 跨域請求詳解——從繁至簡 - 前端 - 掘金什么是jsonp?為什么要用jsonp?JSONP(JSON with Padding)是JSON的一種使用模式,可用于解決主流瀏覽器的跨域數據訪問的問題...
閱讀 982·2021-11-23 09:51
閱讀 2695·2021-08-23 09:44
閱讀 656·2019-08-30 15:54
閱讀 1433·2019-08-30 13:53
閱讀 3101·2019-08-29 16:54
閱讀 2527·2019-08-29 16:26
閱讀 1186·2019-08-29 13:04
閱讀 2313·2019-08-26 13:50