【Zepto源码】offset相关方法
http://www.qdfuns.com/notes/17398/5fdd81ba8a9af7fab63a0583f5416f84.html
offset这个单词的意思是“偏移量”的意思。
本文主要分析的是offset和position方法。
因为是偏移,那么就得有偏移基准,相对谁偏移的问题。
其中offset是相对视口的偏移量(left和top)。
而position是相对其定位的最近的某个祖先元素的偏移量(left和top)。
后者是我们比较熟悉的。
#offsetParent
不管offset还是position方法的源码都用到offsetParent方法。
本文先分析此方法。dom本身就有个属性offsetParent。
表示离某元素最近的定位的祖先元素(position属性值是absolute、relative或fixed)。
因此我们可以用如下的方式来实现:
$.fn.offsetParent = function() {
return this.map(function() {
return this.offsetParent;
});
};
注意上面是用的map,即每个元素都找其offsetParent。并不满足一般情况的“get one, set all.”原则。
某些元素的offsetParent是null的,比如body,比如position为fixed的元素。
因此代码变更为:
$.fn.offsetParent = function() {
return this.map(function() {
return this.offsetParent || document.body;
});
};
但是源码却是这样的:
var rootNodeRE = /^(?:body|html)$/i;
$.fn.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;
});
};
其中正则是用来判断根元素的,比如body。
为何有这个while循环?
估计是为了兼容吧。也许某些浏览器,会把一些显式设置position为static的也当作offsetParent。因为static是position的默认值,我们不认为其是定位元素的。所以进而再找此元素的非static的offsetParent。至于有哪些浏览器,个人不清楚。
#offset
获取或设置元素在视口的位置。
1.获取
可以通过使用obj.getBoundingClientRect来获取位置。
但是获取后的位置是“相对的”,体现在它没有考虑文档页面的滚动情况。如果我们稍微做下修正即可:
$.fn.offset = function() {
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[0]体现了zepto的“get one, set all.”原则。只获取第一个。
window.pageXOffset和window.pageYOffset是window.scrollX和window.scrollY的别名。当然前者兼容更好。
上面的代码没有考虑到this[0]不存在的情形。修改如下:
$.fn.offset = function() {
if (!this.length) return null;
if (document.documentElement !== this[0]&& !$.contains(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)
};
};
其中第2个if考虑的是如果该元素不是页面上的节点之外的情形。比如$({x:1}),或者$('<div></div>')等。
2.设置
有了读取,那么设置就轻松了。
具体思路是:
我们设置位置,只能设置当前元素相对于offsetParent的位置P1。
我们拿到offsetParent的元素相对视口的位置P2,
然后P1+P2,就是当前元素相对于视口的位置P。
代码如下:
$.fn.offset = function(coordinates) {
return this.each(function(index) {
var $this = $(this),
coords = funcArg(this, coordinates, $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);
});
};
需要解释的地方:
1.因为设置,是设置所有,所以要用each方法。
2.之所以return,是为了返回this,方便链式调用。each方法会返回this的。
3.funcArg是内部的辅助函数,保证coordinates也支持函数的形式。之前文章分析过此函数。
4.要设置当前元素的left和top,需要当前元素是定位的。因此有了里面的if。
5.通过css方法来设置position、left、top属性。
offset的完整实现如下:
$.fn.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: coord.top - parentOffset.top,
left: coordinates.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)
};
};
#position
position并没有“设置”,只有获取。
我原先以为,通过css方法,直接取left和top属性即可。
后来一想此元素可能并未定位,因此这种方式不可行。
不过,有了offset方法后,position就轻松了。
其实现思路是,拿到当前元素(第一个元素)的offset,以及其offsetParent的offset,然后一做差,进而可以近似拿到left和top。
之所以是“近似”,因为还要减去当前元素的外边距以及其offsetParent元素的边框厚度。(此处需要画个图来理清上述位置关系)
$.fn.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
};
};
小结一下:
offset是相对视口的偏移量,可以用通过obj.getBoundingClientRect来实现,也需要考虑滚动条的情况。
position来获取相对offsetParent的偏移量,可以通过offset来实现。通过子元素与定位父元素的offset之差来做,需要扣除定位父元素的边框以及子元素margin值。
本文完。

浙公网安备 33010602011771号