【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值。

本文完。

posted @ 2017-04-08 15:25  五艺  阅读(309)  评论(0)    收藏  举报