這篇文章主要想說一下Zepto中與"偏移"相關的一些事,好久好久之前,咱們常常會使用
offset
、position
、scrollTop
、scrollLeft
等方式去改變元素的位置,他們之間有什麼區別,是怎麼實現的呢?接下來咱們一點點去扒開他們的面紗。javascript
原文連接css
源碼倉庫html
offset
、position
兩個api內部的實現都依賴offsetParent
方法,咱們先看一下它是怎麼一回事。java
找到第一個定位過的祖先元素,意味着它的css中的position 屬性值爲「relative」, 「absolute」 or 「fixed」 #offsetParentnode
咱們都知道css屬性position用於指定一個元素在文檔中的定位方式,其初始值是static, css3中甚至還增長了sticky等屬性,不過目前貌似瀏覽器幾乎還未支持。css3
看一下這個例子git
htmlgithub
<div class="wrap">
<div class="child1">
<div class="child2">
<div class="child3"></div>
</div>
</div>
</div>
複製代碼
cssajax
<style> .wrap{ width: 400px; height: 400px; border: solid 1px red; } .child1{ width: 300px; height: 300px; border: solid 1px green; position: relative; padding: 10px; } .child2{ width: 200px; height: 200px; border: solid 1px bisque; } .child3{ width: 100px; height: 100px; border: solid 1px goldenrod; position: absolute; left: 0; top: 0; } </style>
複製代碼
javascriptjson
console.log($('.child3').offsetParent()) // child1
console.log(document.querySelector('.child3').offsetParent) // child1
複製代碼
既然原生已經有了一個offsetParentmdn offsetParent屬性供咱們使用,爲何Zepto還要本身實現一個呢?其實他們之間仍是有些不一樣的,好比一樣是上面的例子,若是child3的display屬性設置爲了none,原生的offsetParent返回的是null,可是Zepto返回的是包含body元素的Zepto對象。
源碼分析
offsetParent: function () {
return this.map(function () {
var parent = this.offsetParent || document.body
while (parent && !rootNodeRE.test(parent.nodeName) && $(parent).css("position") == "static")
parent = parent.offsetParent
return parent
})
}
複製代碼
實現邏輯仍是比較簡單,經過map
方法遍歷當前選中的元素集合,結果是一個數組,每一個項便是元素的最近的定位祖先元素。
首先經過offsetParent
原生DOM屬性去獲取定位元素,若是沒有默認是body節點,這裏其實就能解釋前面的child3設置爲display:none
,原生返回null,可是Zepto獲得的是body了
var parent = this.offsetParent || document.body
複製代碼
再經過一個while
循環若是
html
或者body
元素static
,則再次獲取parent屬性的offsetParent
再次循環。得到當前元素相對於document的位置。返回一個對象含有: top, left, width和height
當給定一個含有left和top屬性對象時,使用這些值來對集合中每個元素進行相對於document的定位。
源碼
offset: function (coordinates) {
if (coordinates) return this.each(function (index) {
var $this = $(this),
coords = funcArg(this, coordinates, index, $this.offset()),
parentOffset = $this.offsetParent().offset(),
props = {
top: coords.top - parentOffset.top,
left: coords.left - parentOffset.left
}
if ($this.css('position') == 'static') props['position'] = 'relative'
$this.css(props)
})
if (!this.length) return null
if (document.documentElement !== this[0] && !$.contains(document.documentElement, this[0]))
return { top: 0, left: 0 }
var obj = this[0].getBoundingClientRect()
return {
left: obj.left + window.pageXOffset,
top: obj.top + window.pageYOffset,
width: Math.round(obj.width),
height: Math.round(obj.height)
}
}
複製代碼
和Zepto中的其餘api相似遵循get one, set all
原則,咱們先來看看獲取操做是如何實現的。
if (!this.length) return null
if (document.documentElement !== this[0] && !$.contains(document.documentElement, this[0]))
return { top: 0, left: 0 }
var obj = this[0].getBoundingClientRect()
return {
left: obj.left + window.pageXOffset,
top: obj.top + window.pageYOffset,
width: Math.round(obj.width),
height: Math.round(obj.height)
}
複製代碼
!this.length
若是當前沒有選中元素,天然就沒有往下走的必要了,直接return掉
當前選中的集合中不是html
元素,而且也不是html
節點子元素。直接返回{ top: 0, left: 0 }
接下來的邏輯纔是重點。首先經過getBoundingClientRect獲取元素的大小及其相對於視口的位置,再經過pageXOffset、pageYOffset獲取文檔在水平和垂直方向已滾動的像素值,相加既獲得咱們最後想要的值。
再看設置操做如何實現以前,先看下面這張圖,或許會有助於理解
if (coordinates) return this.each(function(index) {
var $this = $(this),
coords = funcArg(this, coordinates, index, $this.offset()),
parentOffset = $this.offsetParent().offset(),
props = {
top: coords.top - parentOffset.top,
left: coords.left - parentOffset.left
}
if ($this.css('position') == 'static') props['position'] = 'relative'
$this.css(props)
})
複製代碼
仍是那個熟悉的模式,熟悉的套路,循環遍歷當前元素集合,方便挨個設置,經過funcArg函數包裝一下,使得入參既能夠是函數,也能夠是其餘形式。
經過上面那張圖,咱們應該能夠很清晰的看出,若是要將子元素設置到傳入的coords.left
的位置,那其實
coords.left
那再作個減法,就獲得咱們最終經過css方法須要設置的left和top值啦。
須要注意的是若是元素的定位屬性是static
,則會將其改成relative
定位,相對於其正常文檔流來計算。
獲取對象集合中第一個元素相對於其
offsetParent
的位置。
position: function() {
if (!this.length) return
var elem = this[0],
offsetParent = this.offsetParent(),
offset = this.offset(),
parentOffset = rootNodeRE.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset()
offset.top -= parseFloat($(elem).css('margin-top')) || 0
offset.left -= parseFloat($(elem).css('margin-left')) || 0
parentOffset.top += parseFloat($(offsetParent[0]).css('border-top-width')) || 0
parentOffset.left += parseFloat($(offsetParent[0]).css('border-left-width')) || 0
return {
top: offset.top - parentOffset.top,
left: offset.left - parentOffset.left
}
}
複製代碼
先看一個例子
html
<div class="parent">
<div class="child"></div>
</div>
複製代碼
css
.parent{
width: 400px;
height: 400px;
border: solid 1px red;
padding: 10px;
margin: 10px;
position: relative;
}
.child{
width: 200px;
height: 200px;
border: solid 1px green;
padding: 20px;
margin: 20px;
}
複製代碼
console.log($('.child').position()) // {top: 10, left: 10}
複製代碼
下面分別是父子元素的盒模型以及標註了須要獲取的top的值
接下來咱們來看它怎麼實現的吧,come on!!!
var offsetParent = this.offsetParent(),
// Get correct offsets
// 獲取當前元素相對於document的位置
offset = this.offset(),
// 獲取第一個定位祖先元素相對於document的位置,若是是根元素(html或者body)則爲0, 0
parentOffset = rootNodeRE.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset()
複製代碼
// 相對於第一個定位祖先元素的位置關係不該該包括margin的舉例,因此減去
offset.top -= parseFloat($(elem).css('margin-top')) || 0
offset.left -= parseFloat($(elem).css('margin-left')) || 0
複製代碼
// 祖先定位元素加上border的寬度
parentOffset.top += parseFloat($(offsetParent[0]).css('border-top-width')) || 0
parentOffset.left += parseFloat($(offsetParent[0]).css('border-left-width')) || 0
複製代碼
第四步
// 相減即結果
return {
top: offset.top - parentOffset.top,
left: offset.left - parentOffset.left
}
複製代碼
總體思路仍是用當前元素相對於文檔的位置減去第一個定位祖先元素相對於文檔的位置,但有兩點須要注意的是position這個api要計算出來的值,不該該包括父元素的border
長度以及子元素的margin
空間長度。因此纔會有第二和第三步。
獲取或設置頁面上的滾動元素或者整個窗口向右滾動的滾動距離。
scrollLeft: function (value) {
if (!this.length) return
var hasScrollLeft = 'scrollLeft' in this[0]
if (value === undefined) return hasScrollLeft ? this[0].scrollLeft : this[0].pageXOffset
return this.each(hasScrollLeft ?
function () { this.scrollLeft = value } :
function () { this.scrollTo(value, this.scrollY) })
}
複製代碼
首先判斷當前選中的元素是否支持scrollLeft特性。
若是value沒有傳進來,又支持hasScrollLeft
特性,就返回第一個元素的hasScrollLeft
值,不支持的話返回第一個元素的pageXOffset
值。
pageXOffset是scrollX的別名,而其表明的含義是返回文檔/頁面水平方向滾動的像素值
傳進來了value
就是設置操做了,支持scrollLeft
屬性,就直接設置其值便可,反之須要用到scrollTo,固然設置水平方向的時候,垂直方向仍是要和以前的保持一致,因此傳入了scrollY
做爲
獲取或設置頁面上的滾動元素或者整個窗口向下滾動的距離。
scrollTop: function(value) {
if (!this.length) return
var hasScrollTop = 'scrollTop' in this[0]
if (value === undefined) return hasScrollTop ? this[0].scrollTop : this[0].pageYOffset
return this.each(hasScrollTop ?
function() { this.scrollTop = value } :
function() { this.scrollTo(this.scrollX, value) })
},
複製代碼
能夠看出基本原理和模式與scrollLeft
一致,就再也不一一解析。
以上就是Zepto中與"偏移"相關的幾個api的解析,歡迎指出其中的問題和有錯誤的地方。
...
ie模塊
data模塊
form模塊
zepto模塊
event模塊
ajax模塊