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

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
循环如果
- parent元素存在
- parent元素不是
html
或者body
元素 - parent元素的display属性是
static
,则再次获取parent属性的offsetParent
再次循环。
offset
获得当前元素相对于document的位置。返回一个对象含有: top, left, width和height
当给定一个含有left和top属性对象时,使用这些值来对集合中每一个元素进行相对于document的定位。
- offset() ⇒ object
- offset(coordinates) ⇒ self v1.0+
- offset(function(index, oldOffset){ ... }) ⇒
源码
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
的位置,那其实
- 父元素(假设父元素是定位元素)相对文档的左边距(parentOffset.left)
- 子元素相对父元素的左边距(left)
- 相加得到的就是入参
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 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
获取或设置页面上的滚动元素或者整个窗口向右滚动的滚动距离。
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的解析,欢迎指出其中的问题和有错误的地方。
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。