jQuery源码分析(十八): DOM操作尺寸获取

HTML 2016-12-06

这不是我要的高

起步

来看看jq中关于元素尺寸的操作。

元素大小

让人想起css中的盒子模型。来看看页面中和dom对象对应的关于尺寸的定义。一图胜千言。

客户区域大小

clientWidth、clientHeight

242021048359691.png

clientWidth/clientHeight: 用于描述元素的内尺寸:元素内容 + 两边内边距。(IE下实际包括边框)。

clientWidth = width+padding(left、right)
clientHeight = height+padding(top、bottom)

偏移量

offsetWidth offsetHeight offsetLeft offsetTop

242021021483205.png

offsetHeight/offsetWidth: 表述元素的外尺寸:元素内容+内边距+边框(不包括外边距)

offsetLeft/offsetTop: 表示该元素的左上角(边框外边缘)与已定位的父容器(offsetParent对象)左上角的距离。

offsetParent元素是指元素最近的定位(relative,absolute)祖先元素,可递归上溯。

offsetWidth =  border-left-width + padding-left + width + padding-right + border-right-width;
offsetHeight =  border-top-width + padding-top + height + padding-bottom + border-bottom-width;

滚动大小

scrollWidth、scrollHeight、scrollLeft、scrollTop

242021067376237.png

scrollHeight/scrollWidth: 元素内容的总高度或宽度

scrollLeft/scrollTop:是指元素滚动条位置,它们是可写的(被隐藏的内容区域左侧/上方的像素)

scrollHeight:是元素的padding加元素内容的高度。这个高度与滚动条无关,是内容的实际高度。

计算方式 :scrollHeight = topPadding + bottomPadding + 内容margix box的高度

offsetParent是指元素最近的定位(relative,absolute)祖先元素,递归上溯,如果没有祖先元素是定位的话,会返回null。

在浏览器中的区别在于:

IE6、IE7 认为scrollHeight 是网页内容实际高度,可以小于clientHeight。

FF、Chrome 认为scrollHeight 是网页内容高度,不过最小值是clientHeight。

浏览器窗口的滚动条位置:window对象的 pageXoffsetpageYoffset , IE 8及更早版本可以通过scrollLeft和scrollTop属性获得滚动条位置。

一些细节

document.documentElement与document.body的区别

document.body 是 DOM 中 Document 对象里的 body 节点
document.documentElement 是文档对象根节点(html)的引用

Document对象是每个DOM树的根,但是它并不代表树中的一个HTML元素,document.documentElement属性引用了作为文档根元素的html标记,document.body属性引用了body标记 我们这里获取常见的三个值(scrollWidth、offsetWidth和clientwidth)来比较一下

document.body.scrollWidth = document.documentElement.scrollWidth
document.body.offsetWidth = document.documentElement.offsetWidth
document.body.clientwidth = document.documentElement.clientwidth - document.body.borderWidth(body的边框宽度)

当我们给body设置了一个宽度的时候,区别就出来了。

不过一般来说,我们不会给document.documentElement来设置边框,所以这里的 clientwidthoffsetWidth 一致。

document.body.scrollWidth返回body的宽度。基于 webkit 的浏览器(Chrome和Safari)返回的是整个文档的宽度,也就是和 document.documentElement.scrollWidth 一致,opera 和 FF 返回的就是标准的 body 的 scrollWidth 个人觉得 opera 和 FF 算是比较合理的。

document.body.offsetWidth返回body的offsetWidth。 document.body.clientWidth 返回body的clientWidth(不包含边框),clientWidth = offsetWidth - borderWidth前面的例子,会发现body和documentElement的有些值是相等的,这并不是表示他们是等同的。而是因为当我们没有给body设置宽度的时候,document.body默认占满整个窗口宽度,于是就有:

document.body.scrollWidth = document.documentElement.scrollWidth
document.body.offsetWidth = document.documentElement.offsetWidth
document.body.clientWidth = document.documentElement.clientWidth - document.body.borderWidth(body的边框宽度)

为什么offsetWidth始终比clientWidth大呢? 原因就在于这个“边线”。在FF1.06+和IE6.0+上,有这样的区别:

clientWidth = width + padding
clientHeight = height + padding
offsetWidth = width + padding + border
offsetHeight = height + padding + border

当然,如果出现的滚动条,offsetWidth也会包含滚动条的宽度,而clientWidth是不包含滚动条的宽度的。 ele.style.border = 0后这两个值就相等了。

如果出现的滚动条,offsetWidth也会包含滚动条的宽度,而clientWidth不包含滚动条的宽度的。

IE的document.documentElement.offsetWidth不同版本之间又差异,微软都放弃IE了,不要看区别了,省的越看越混。

尺寸获取

jq对窗口大小有六种相似的方法:

jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
    jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) {
        jQuery.fn[ funcName ] = function( margin, value ) {
            //执行代码
        }
    }
}

一共创建了innerHeight, innerWidth, height, width, outerHeightouterWidth

.width()

为匹配的元素集合中获取第一个元素的当前计算宽度值。

return access( this, function( elem, type, value ) {
    var doc;

    if ( jQuery.isWindow( elem ) ) {
        // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there
        // isn't a whole lot we can do. See pull request at this URL for discussion:
        // https://github.com/jquery/jquery/pull/764
        return elem.document.documentElement[ "client" + name ];
    }

    // Get document width or height
    if ( elem.nodeType === 9 ) {
        doc = elem.documentElement;

        // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],
        // whichever is greatest
        return Math.max(
            elem.body[ "scroll" + name ], doc[ "scroll" + name ],
            elem.body[ "offset" + name ], doc[ "offset" + name ],
            doc[ "client" + name ]
        );
    }

    return value === undefined ?
        // Get width or height on the element, requesting but not forcing parseFloat
        jQuery.css( elem, type, extra ) :

        // Set width or height on the element
        jQuery.style( elem, type, value, extra );
}, type, chainable ? margin : undefined, chainable, null );

普通元素和非普通元素:非普通元素是指window,document这些 元素对象;普通元素是指除window,document之外的元素,如:div。

.css(width) 和 .width()之间的区别:

对于非普通元素,只能使用 .width();对于普通的元素 ,他们的作用相同;后者返回一个没有单位的数值(例如,400),前者是返回带有完整单位的字符串(例如,'400px')。当一个元素的宽度需要数学计算的时候推荐使用.width() 方法。

非普通元素的获取

window

$(window).width();   //浏览器窗口

对应代码:

if ( jQuery.isWindow( elem ) ) {
    return elem.document.documentElement[ "client" + name ];
}

返回就是:document.documentElement["clientWidth"]

document

$(document).width();   //HTML文档窗口

取最大值,因为可以带卷滚条溢出

if ( elem.nodeType === 9 ) {
    doc = elem.documentElement;

    // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],
    // whichever is greatest
    return Math.max(
        elem.body[ "scroll" + name ], doc[ "scroll" + name ],
        elem.body[ "offset" + name ], doc[ "offset" + name ],
        doc[ "client" + name ]
    );
}

普通元素取值

普通元素取值,即.width() 里面参数为空时,是调用jQuery.css( elem, type, extra )方法的。

因为有些样式不是简单的读写属性就可以的,比如width就不是简单地读取el.style.width。为了解决这个问题,jquery定义了一个属性 $.cssHooks,这里可以自定义对某个属性的get和set操作。而且jquery中就是用cssHooks来处理某些特殊属性的。

钩子$.cssHooks对css的操作统一的API调用,操作的属性是:

  • borderWidth: Object
  • height: Object
  • margin: Object
  • opacity: Object
  • padding: Object
  • width: Object

width,height的钩子方法

jQuery.each([ "height", "width" ], function( i, name ) {
    jQuery.cssHooks[ name ] = {
        get: function( elem, computed, extra ) {
            if ( computed ) {

                // Certain elements can have dimension info if we invisibly show them
                // but it must have a current display style that would benefit
                return rdisplayswap.test( jQuery.css( elem, "display" ) ) && elem.offsetWidth === 0 ?
                    jQuery.swap( elem, cssShow, function() {
                        return getWidthOrHeight( elem, name, extra );
                    }) :
                    getWidthOrHeight( elem, name, extra );
            }
        },

        set: function( elem, value, extra ) {
            var styles = extra && getStyles( elem );
            return setPositiveNumber( elem, value, extra ?
                augmentWidthOrHeight(
                    elem,
                    name,
                    extra,
                    jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
                    styles
                ) : 0
            );
        }
    };
});

get 方法:

  1. 节点隐藏等情况下,height、width等获取值不准,此时需利用jQuery.swap方法来获得准确值
  2. getWidthOrHeight获取准确值
//用于读取,会根据extra加上augmentWidthOrHeight增量
function getWidthOrHeight( elem, name, extra ) {

    // Start with offset property, which is equivalent to the border-box value
    // 这里直接用offsetWidth/offsetHeight返回的borderbox级别值作为基础值
    //因此下面需要调整,valueIsBorderBox默认值为true,表示为border-box
    var valueIsBorderBox = true,
        val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
        styles = getStyles( elem ),
        // 只有支持boxSizing属性,且为border-box,isBorderBox才为true,否则要调整val
        isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box";

    // Some non-html elements return undefined for offsetWidth, so check for null/undefined
    // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
    // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
    // svg 和 MathML 可能会返回 undefined,需要重新求值
    if ( val <= 0 || val == null ) {
        // Fall back to computed then uncomputed css if necessary
        // 直接获取 width/height 作为基础值,若之后调用elem.style,说明support.boxSizingReliable()一定为false
        val = curCSS( elem, name, styles );
        if ( val < 0 || val == null ) {
            val = elem.style[ name ];
        }

        // Computed unit is not pixels. Stop here and return.
        // 匹配到非px且带单位的值,则直接退出
        if ( rnumnonpx.test(val) ) {
            return val;
        }

        // Check for style in case a browser which returns unreliable values
        // for getComputedStyle silently falls back to the reliable elem.style
        // valueIsBorderBox意思是得到的value是borderbox级别的,由于调整为了curCSS取值
        //因此,必须要isBorderBox为true,不可靠值当做content级别处理(因为border、padding容易获取到准确值,val === elem.style[ name ]除外)
        valueIsBorderBox = isBorderBox &&
            ( support.boxSizingReliable() || val === elem.style[ name ] );

        // Normalize "", auto, and prepare for extra
        // 强制去单位,"auto"等字符串变为0
        val = parseFloat( val ) || 0;
    }

    // Use the active box-sizing model to add/subtract irrelevant styles
    return ( val +
        augmentWidthOrHeight(
            elem,
            name,
            // 若没指定,默认值跟盒模型一致
            extra || ( isBorderBox ? "border" : "content" ),
            // 表示基数val是否为borderBox,extra和它一致说明无需累加
            valueIsBorderBox,
            styles
        )
    ) + "px";
}

尺寸设值

当调用 .width(value) 方法的时候,这个“value”参数可以是一个字符串(数字加单位)或者是一个数字。如果这个“value”参数只提供一个数字,如果没有给定明确的,jQuery会自动加上单位 px 。如果只提供一个字符串,任何有效的CSS尺寸都可以为宽度赋值(就像100px, 50%, 或者 auto)。注意在现代浏览器中,CSS宽度属性不包含padding, border, 或者 margin。除非box-sizing CSS属性被使用。

从钩子可以看到,当需要给宽高设值时采用 setPositiveNumber(elem, value, extra) ,自动补单位px:

function setPositiveNumber( elem, value, subtract ) {
    var matches = rnumsplit.exec( value );
    return matches ?
        // Guard against undefined "subtract", e.g., when used as in cssHooks
        // 保证非负值,保留单位,subtract可以指定需要减去的值
        Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) :
        value;
}

偏移算法

默认都统一是采用 offsetWidth 或者 offsetHeight 取值,上面有关于这两个尺寸的设置:

offsetWidth =  border-left-width + padding-left + width + padding-right + border-right-width;
offsetHeight =  border-top-width + padding-top + height + padding-bottom + border-bottom-width;

这是在不再考虑 box-sizing 的情况下, jq有两个获取位置的方法 .offset().position() :

.offset()

方法允许我们检索一个元素相对于文档(document)的当前位置,它和.position()的差别在于:.position()是相对于相对于父级元素的位移

该函数返回一个坐标对象(Object):{ top: 0, left: 0 }

top: box.top + win.pageYOffset - docElem.clientTop,
left: box.left + win.pageXOffset - docElem.clientLeft

当通过全局操作(特别是通过拖拽操作)将一个新的元素放置到另一个已经存在的元素的上面时,若要取得这个新的元素的位置,那么使用 .offset() 更合适。

jQuery.fn.extend({
    //相对页面(含滚动区)的坐标对象{left,top},可读可写,参数可为函数
    offset: function( options ) {
        if ( arguments.length ) {
            return options === undefined ?
                this :
                // 对所有元素设置
                this.each(function( i ) {
                    jQuery.offset.setOffset( this, options, i );
                });
        }

        var docElem, win,
            elem = this[ 0 ],
            box = { top: 0, left: 0 },
            doc = elem && elem.ownerDocument;

        if ( !doc ) {
            return;
        }

        docElem = doc.documentElement;

        // Make sure it's not a disconnected DOM node
        // 文档片段元素按照{left:0, top:0}返回
        if ( !jQuery.contains( docElem, elem ) ) {
            return box;
        }

        // Support: BlackBerry 5, iOS 3 (original iPhone)
        // If we don't have gBCR, just use 0,0 rather than error
        if ( typeof elem.getBoundingClientRect !== strundefined ) {
            // 得到相对视口的坐标
            box = elem.getBoundingClientRect();
        }
        win = getWindow( doc );
        return {
            // + 滚动距离 - 低版本IE的(2,2)修正
            top: box.top + win.pageYOffset - docElem.clientTop,
            left: box.left + win.pageXOffset - docElem.clientLeft
        };
    },
});

.position()

.position()方法可以取得元素相对于父元素的偏移位置。与.offset()不同, .offset()是获得该元素相对于documet的当前坐标,当把一个新元素放在同一个容器里面另一个元素附近时,用.position()更好用。.position()返回一个包含 top 和 left 属性的对象.Position是一个相对父元素的定位所以实际的距离还要跟当然用了定位的属性有关系。

position: function() {
    if ( !this[ 0 ] ) {
        return;
    }

    var offsetParent, offset,
        elem = this[ 0 ],
        parentOffset = { top: 0, left: 0 };

    // Fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is its only offset parent
    if ( jQuery.css( elem, "position" ) === "fixed" ) {
        // Assume getBoundingClientRect is there when computed position is fixed
        // fixed的top和left是以视口为基准,直接取坐标
        offset = elem.getBoundingClientRect();

    } else {
        // Get *real* offsetParent
        // offsetParent()方法会把冒泡到body节点的(且body也无定位)按照html节点处理
        offsetParent = this.offsetParent();

        // Get correct offsets
        // 得到自身和offsetParent的offset()
        offset = this.offset();
        if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) {
            parentOffset = offsetParent.offset();
        }

        // Add offsetParent borders
        // 修正,因为是到offsetParent的border内侧
        //如果元素本身带滚动条,并且滚动了一段距离,那么两者间实际的偏移应该更多
        parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true );
        parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true );
    }

    // Subtract parent offsets and element margins
    // 两offset()相减,margin需作为元素一部分,去掉
    return {
        top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ),
        left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true )
    };
},

我们分析下几种情况:

Fixed:生成绝对定位的元素,相对于浏览器窗口进行定位,明显这样的定位的父元素就的文档了,也就没有父元素的相对点,但实际上我们根据盒子模型来算的话,实际上的定位出了left还有一个margin是会影响结果的,所以真实的定位值。

Left = ele.offset().left  –jQuery.css(elem, " marginLeft ", true)
Top = ele.offset().top –jQuery.css(elem, " marginTop", true)

这样的算法才是精确的。

如果元素采用了Static、absolute、relative属性,那么都是跟定位有关系,都有父级的相对点,这样我们可以通过获取元素的offsetParent得到最近的父元素。这样的算法就比较简单了,我们可以获取元素相对文档的最真实的坐标减去父节点相对文档的坐标,这里涉及了2个元素的盒子模型所以都需要加上影响的属性。父节点的精确值要加上border的处理。

parentOffset.top += jQuery.css(offsetParent[0], "borderTopWidth", true);
parentOffset.left += jQuery.css(offsetParent[0], "borderLeftWidth", true);

所以最终的计算公式:

top: offset.top - parentOffset.top - jQuery.css(elem, "marginTop", true),
left: offset.left - parentOffset.left - jQuery.css(elem, "marginLeft", true)

本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

如果对您有用,您的支持将鼓励我继续创作!