前言


这篇文章主要想说一下Zepto中与"偏移"相关的一些事,很久很久以前,我们经常会使用​​offset​​​、​​position​​​、​​scrollTop​​​、​​scrollLeft​​等方式去改变元素的位置,他们之间有什么区别,是怎么实现的呢?接下来我们一点点去扒开他们的面纱。


​源码仓库​

向Zepto学习关于_API

offsetParent

offset​position​两个api内部的实现都依赖​offsetParent​方法,我们先看一下它是怎么一回事。


找到第一个定位过的祖先元素,意味着它的css中的position 属性值为“relative”, “absolute” or “fixed” ​​#offsetParent​


我们都知道css属性position用于指定一个元素在文档中的定位方式,其初始值是static, css3中甚至还增加了sticky等属性,不过目前貌似浏览器几乎还未支持。

看一下这个例子

html

<div class="wrap">
<div class="child1">
<div class="child2">
<div class="child3"></div>
</div>
</div>
</div>

css

<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>

javascript

console.log($('.child3').offsetParent()) // child1
console.log(document.querySelector('.child3').offsetParent) // child1

既然原生已经有了一个offsetParent​mdn 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​​循环如果

  1. parent元素存在
  2. parent元素不是​​html​​​或者​​body​​元素
  3. parent元素的display属性是​​static​​​,则再次获取parent属性的​​offsetParent​​再次循环。

offset


获得当前元素相对于document的位置。返回一个对象含有: top, left, width和height



当给定一个含有left和top属性对象时,使用这些值来对集合中每一个元素进行相对于document的定位。


  1. offset() ⇒ object
  2. offset(coordinates) ⇒ self v1.0+
  3. offset(function(index, oldOffset){ ... }) ⇒

​#offset​

源码

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: - ,
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: + 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: + window.pageYOffset,
width: Math.round(obj.width),
height: Math.round(obj.height)
}
  1. ​!this.length​​如果当前没有选中元素,自然就没有往下走的必要了,直接return掉
  2. 当前选中的集合中不是​​html​​元素,并且也不是​​html​​节点子元素。直接返回​​{ top: 0, left: 0 }​
  3. 接下来的逻辑才是重点。首先通过​​getBoundingClientRect​​获取元素的大小及其相对于视口的位置,再通过​​pageXOffset​​、​​pageYOffset​​获取文档在水平和垂直方向已滚动的像素值,相加既得到我们最后想要的值。

再看设置操作如何实现之前,先看下面这张图,或许会有助于理解

向Zepto学习关于_Zepto.js_02

if (coordinates) return this.each(function(index) {
var $this = $(this),
coords = funcArg(this, coordinates, index, $this.offset()),
parentOffset = $this.offsetParent().offset(),
props = {
top: - ,
left: coords.left - parentOffset.left
}

if ($this.css('position') == 'static') props['position'] = 'relative'
$this.css(props)
})

还是那个熟悉的模式,熟悉的套路,循环遍历当前元素集合,方便挨个设置,通过funcArg函数包装一下,使得入参既可以是函数,也可以是其他形式。

通过上面那张图,我们应该可以很清晰的看出,如果要将子元素设置到传入的​​coords.left​​的位置,那其实

  1. 父元素(假设父元素是定位元素)相对文档的左边距(parentOffset.left)
  2. 子元素相对父元素的左边距(left)
  3. 相加得到的就是入参​​coords.left​

那再做个减法,就得到我们最终通过css方法需要设置的left和top值啦。

需要注意的是如果元素的定位属性是​​static​​​,则会将其改为​​relative​​定位,相对于其正常文档流来计算。

position


获取对象集合中第一个元素相对于其​​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
+= parseFloat($(offsetParent[0]).css('border-top-width')) || 0
parentOffset.left += parseFloat($(offsetParent[0]).css('border-left-width')) || 0
return {
top: offset.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的值

向Zepto学习关于_css_03向Zepto学习关于_CSS_04

接下来我们来看它怎么实现的吧,come on!!!

  1. 第一步
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()
  1. 第二步
// 相对于第一个定位祖先元素的位置关系不应该包括margin的举例,所以减去
offset.top -= parseFloat($(elem).css('margin-top')) || 0
offset.left -= parseFloat($(elem).css('margin-left')) || 0
  1. 第三步
// 祖先定位元素加上border的宽度
+= parseFloat($(offsetParent[0]).css('border-top-width')) || 0
parentOffset.left += parseFloat($(offsetParent[0]).css('border-left-width')) || 0

第四步

// 相减即结果
return {
top: offset.top - ,
left: offset.left - parentOffset.left
}

整体思路还是用当前元素相对于文档的位置减去第一个定位祖先元素相对于文档的位置,但有两点需要注意的是position这个api要计算出来的值,不应该包括父元素的​​border​​​长度以及子元素的​​margin​​空间长度。所以才会有第二和第三步。

scrollLeft


获取或设置页面上的滚动元素或者整个窗口向右滚动的滚动距离。


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


获取或设置页面上的滚动元素或者整个窗口向下滚动的距离。


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的解析,欢迎指出其中的问题和有错误的地方。


参考

​读Zepto源码之属性操作​

​scrollTo​

​scrollLeft​

​pageXOffset​

...