jQuery源码分析之offset,position,offsetParent方法以及源码中常见的cssHooks,swap代码

jQuery.offset.setOffset源码分析:

setOffset: function( elem, options, i ) {
		var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,
			//获取该元素的position属性
			position = jQuery.css( elem, "position" ),
			//把当前元素包装为jQuery元素!
			curElem = jQuery( elem ),
			props = {};
         //如果当前元素是static类型,那么把这个DOM元素的position设置为relative!
		 //以防把top,left属性设置到static元素上面?static没有left?top?
		// set position first, in-case top/left are set even on static elem
		if ( position === "static" ) {
			elem.style.position = "relative";
		}
        //调用当前jQuery对象的offset方法获取到offset属性!也就是设置和文档的偏移之前首先获取到文档的偏移!
		curOffset = curElem.offset();

		//获取DOM的top,left属性,但是这个top,left不是options中left和top属性!
		curCSSTop = jQuery.css( elem, "top" );
		curCSSLeft = jQuery.css( elem, "left" );

		//如果元素的postion是absolute或者fixed,同时top或者left是auto!
		calculatePosition = ( position === "absolute" || position === "fixed" ) &&
			jQuery.inArray("auto", [ curCSSTop, curCSSLeft ] ) > -1;
		// need to be able to calculate position if either top or left is auto and position is either absolute or fixed
		if ( calculatePosition ) {
	//获取当前元素的position属性!也就是相对于被定位的祖辈元素的位置!也就是如果postion是absolute或者fixed,同时left,right是auto
	//那么就是相对于被定位的父元素来说的!(因为如果本身的position是static那么已经被转为relative了,relative是相对于absolute定位的!)
			curPosition = curElem.position();
			curTop = curPosition.top;
			curLeft = curPosition.left;
		} else {
			//否则直接把DOM元素已经具有的left和top属性解析为浮点类型,如果没有就是0!
			curTop = parseFloat( curCSSTop ) || 0;
			curLeft = parseFloat( curCSSLeft ) || 0;
		}
        //如果是函数,直接调用函数,函数中context是DOM元素,第一个参数是DOM下标,第二个参数是当前DOM元素的offset的值!
		if ( jQuery.isFunction( options ) ) {
			options = options.call( elem, i, curOffset );
		}
        //如果传入的参数有top属性,那么把props的top属性设置为
		if ( options.top != null ) {
			props.top = ( options.top - curOffset.top ) + curTop;
		}
		//如果传入的参数有left属性,那么把props的left属性设置为
		if ( options.left != null ) {
			props.left = ( options.left - curOffset.left ) + curLeft;
		}
       //如果
		if ( "using" in options ) {
			options.using.call( elem, props );
		} else {
			curElem.css( props );
		}
	}
测试总结:

(1)left,top是相对于最近的一个定位为absolute或者relative的父元素来说的!fixed是相对于浏览器窗口来定位的!
(2)static默认值。没有定位,元素出现在正常的流中(忽略 top, bottom, left, right 或者 z-index 声明),如果设置的时候元素是static那么首先把它变成relative定位!

(3)代码props.top = ( options.top - curOffset.top ) + curTop;我当时就纳闷了,为什么要这么写代码?看下面的例子:

<div id="content" style="top:10px;"></div>

假如有上面这一段代码,显然这时候该元素的offsetTop=8(border的margin)+10(top)=18px,那么我现在要写成offset({top:100})元素要怎么移动呢?很显然在原来的位置上向下移动82px就可以了,因为现在已经有了18px了。那么怎么做呢?我们通过调用jQuery.css方法,因为该方法的底层是调用了元素的style属性完成的!所以只要给该元素的style["top"]设置为82+10=92px就可以了!这时候通过$("#content")[0].getBoundingClientRect().top也能得到结果是100px!

问题1:如果position为absolute或者fixed,这时候通过jQuery.css获取left/top值可能是auto,其中left/top表示相对于上一个定位的父元素的距离,那么如何设置该元素的offset?

解答:我们首先获取该元素相对于父元素的距离,通过position方法来获取。然后通过options.top-curOffset.top表示还要移动的距离,然后加上本身有的距离也就是top值,从而得到。而本身具有的距离如果jQuery.css获取不到具体的值,那么通过其position方法来获取具体的值!

offset方法源码分析:

//获取当前元素相对于文档的偏移量
	offset: function( options ) {
		//如果有参数表示设置调用对象的相应属性!
		if ( arguments.length ) {
			return options === undefined ?
				this :
				this.each(function( i ) {
					jQuery.offset.setOffset( this, options, i );
				});
		}
		var docElem, win,
			box = { top: 0, left: 0 },
			//获取调用对象的第一个元素
			elem = this[ 0 ],
				//获取第一个元素的document对象
			doc = elem && elem.ownerDocument;
         //如果document对象不存在直接返回
		if ( !doc ) {
			return;
		}
    //否则获取documentElement对象
		docElem = doc.documentElement;
        //如果不再文档中表示elem是一个脱离文档的DOM节点
		// Make sure it's not a disconnected DOM node
		if ( !jQuery.contains( docElem, elem ) ) {
			return box;
		}
		// If we don't have gBCR, just use 0,0 rather than error
		// BlackBerry 5, iOS 3 (original iPhone)
		//这个方法返回一个矩形对象,包含四个属性:left、top、right和bottom。分别表示元素各边与页面上边和左边的距离。
		if ( typeof elem.getBoundingClientRect !== strundefined ) {
			box = elem.getBoundingClientRect();
		}
		//获取window对象
		win = getWindow( doc );
		//返回的对象的top属性是elem.getBoundingClientRect+window.pageYoffset||documentElement.scrollTop-documentElement.clientTop
		return {
			top: box.top  + ( win.pageYOffset || docElem.scrollTop )  - ( docElem.clientTop  || 0 ),
			left: box.left + ( win.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 )
		};
	}
总结:

(1)offsetTop的属性的获取是通过elem.getBoundingClientRect.top+window.pageYoffset||documentElement.scrollTop-documentElement.clientTop
(2)offsetLeft的属性获取是通过elem.getBoundingClientRect.left+window.pageXoffset||documentElement.scrollLeft-documentElement.clientLeft

(3)IE的getBoundingClientRect是从(2,2)开始计算的,所以要做兼容,也就是减去documentElement的clientTop,document.documentElement.clientTop对于非IE浏览器来说是0,但是相对于IE浏览器来说是2。详见点击打开链接

(4)这里用的是window.pageYOffset(IE专属)和documentElement的scrollTop,而没有用document.body.scrollTop!

(5)要想弄懂offset方法必须要立即offsetParent,他返回距离当前元素最近的并且进行过定位的容器元素,如果没有定位的元素那么返回跟元素,标准模式下为html,怪异模式下是body元素,当容器的display设为none时,offsetParent是null(IE Opera除外)

(6)pageXOffset和pageYOffset表示整个页面的滚动值!

注意:offset只是获取调用对象的第一个DOM元素相对于文档的偏移值,设置的时候是逐个设置的!

position方法源码分析:

	position: function() {
		//如果调用者第一个元素不存在,那么直接返回!
		if ( !this[ 0 ] ) {
			return;
		}
		var offsetParent, offset,
		parentOffset = { top: 0, left: 0 },
			//获取第一个调用对象
		elem = this[ 0 ];
         //如果第0个元素的position是fixed,那么offset就是调用该对象的getBoundingClientRect方法
		 //fixed定位的元素的offset是相对于window来说的,因为window是唯一一个offsetParent
		// fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is its only offset parent
		if ( jQuery.css( elem, "position" ) === "fixed" ) {
			// we assume that getBoundingClientRect is available when computed position is fixed
			offset = elem.getBoundingClientRect();
		} else {
			// Get *real* offsetParent
			//查找距离当前元素最近的被定位的父元素
			offsetParent = this.offsetParent();
			//获取当前元素相对于文档的偏移量
			// Get correct offsets
			offset = this.offset();
		  //如果最近的被定位的元素的不是html元素,那么获取该元素也就是当前元素的父元素相对于文档的偏移量!
			if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) {
				parentOffset = offsetParent.offset();
			}
			// Add offsetParent borders
			//加上offsetParent的borderTopWidth和borderLeftWidth属性!要记住border也是有宽度的!
			parentOffset.top  += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true );
			parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true );
		}

		// Subtract parent offsets and element margins
		// note: when an element has margin: auto the offsetLeft and marginLeft
		// are the same in Safari causing offset.left to incorrectly be 0
		return {
//返回值为当前元素的top属性减去有定位的父元素的top属性(包括被定位的父元素的borderTopWidth属性),同时还要减去当前元素的marginTop属性
			top:  offset.top  - parentOffset.top - jQuery.css( elem, "marginTop", true ),
			left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true)
		};
	}
position测试总结:

(1)如果调用position的调用对象的第一个DOM是fixed定位的,那么他的position是直接相对于window对象定位的,直接调用getBoundingClientRect减去他的marginTop属性就可以了,因为parentOffset为0!

(2)如果不是第一种情况,那么首先获取当前元素的offset也就是相对于文档的top和left,然后获取距离当前元素最近的被定位的元素,如果这个元素不是html那么获取该元素的offset,也就是获取当前元素的最近的被定位的父元素相对于文档的距离,两者相减之后再减去当前元素相对于父元素的marginTop就获取到了position属性!之所以要减去marginTop属性是因为对于文档流来说如果margin是正数表示是外扩的,反之是内缩的!

(3)获取元素到父元素的的距离的方法=当前元素到文档的距离(getBoundingClientRect到内容结束,也就是到border结束!)-父元素到文档的距离(getBoundClientRect)-元素的borderWidth-子元素的marginTop属性!

if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) {  
            parentOffset = offsetParent.offset();  
        }  
        // Add offsetParent borders  
        //加上offsetParent的borderTopWidth和borderLeftWidth属性!要记住border也是有宽度的!  
        parentOffset.top  += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true );  
        parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true );  

注意:position关注的是调用对象的第一个DOM对象的距离,如果第一个DOM是html那么offset就会是0,如果是其它元素就会获取该元素的offset值。总之,他仅仅获取第一个元素的坐标。而offset获取的也是第一个元素的左边,但是offset可以设置,但是position是只读的!

offsetParent源码分析:

offsetParent: function() {
		return this.map(function() {
			//this指向DOM元素,首先获取到DOM元素的offsetParent对象
			var offsetParent = this.offsetParent || docElem;
            //如果offsetParent存在,同时offsetParent的nodeName不是html,同时该offsetParent对象的position是static
			//那么不断获取到offsetParent对象,jQuery.css调用的是curCss因为<span style="font-family: Consolas, 'Courier New', Courier, mono, serif; font-size: 12px; line-height: 18px; background-color: rgb(248, 248, 248);">jQuery.cssHooks["position"]不存在!</span>
	while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position" ) === "static" ) ) {
				offsetParent = offsetParent.offsetParent;
			}
			//最后返回offsetParent对象放入数组集合!
			return offsetParent || docElem;
		});
	}
});

offsetParent总结:

(1)如果父元素的position是static,那么就要不断往上寻找,默认是html元素!同时返回的是所有调用对象的offsetParent对应的集合.jQuery.map最后使用了concat来完成!

cssHooks有的属性见下面几段源码:( 可以直接打印jQuery.cssHooks["xxx"]得到函数)

top,left所在的Hooks如下:

目地:在除了低版本的Safari的浏览器中通过getComputedStyle获取元素的top/left值都会返回具体的像素值。如果是低版本safari浏览器就要特殊处理。特殊的处理逻辑就是如果返回了如20%等,那么就调用position方法获取相应的top/left值!因为position最终调用了offset,而offset调用了getBoundingClientRect,该方法会返回有效的像素值!

jQuery.each( [ "top", "left" ], function( i, prop ) {
	jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,
		function( elem, computed ) {
			if ( computed ) {
				computed = curCSS( elem, prop );
				// if curCSS returns percentage, fallback to offset
				return rnumnonpx.test( computed ) ?
					jQuery( elem ).position()[ prop ] + "px" :
					computed;
			}
		}
	);
});

opacity所在的Hooks:

目的:我们必须通过opacity获取到一个数字,所以如果没有获取到那么就是1,否则就是获取到的值!

cssHooks: {
		opacity: {
			get: function( elem, computed ) {
				if ( computed ) {
					// We should always get a number back from opacity
					var ret = curCSS( elem, "opacity" );
					return ret === "" ? "1" : ret;
				}
			}
		}

height和width所在的Hooks:

目地:针对display为none,和display不是table-cell/table-caption做特殊处理!

		        var rdisplayswap = /^(none|table(?!-c[ea]).+)/;
				//打印true
				//alert(rdisplayswap.test("none"));
				//打印true,table后面不是-ca和-ce
				//alert(rdisplayswap.test("table-name"));
如果在获取属性height/width时候,该元素的display不是table-cell/table-caption,同时该元素的offsetWidth是0。那么我们首先把该元素的position变成absolute,visibility变成hidden,display变成block

实例css方法中有一句代码为: jQuery.css( elem, name[ i ], false, styles ),他继续调用了jQuery.css,而jQueyr.css中代码为

if ( hooks && "get" in hooks ) {
			val = hooks.get( elem, true, extra );
		}
在结合我们下面获取height/width的用法,知道传入get方法的computed为true,extra为false,所以调用getWidthOrHeight方法传入的extra是false,在getWidthOrHeight方法中对box-sizing进行判断,首先获取到offsetWidth。如果是content-box那么我们获取到的offsetWidth=width+2padding+2borderWidth,但是我们只是获取width所以需要减去padding!因此传入argumentWidthOrHeight第三个参数是content,第四个参数是true,所以在argumentWidthOrHeigth中就相当于用offsetWidth-paddingLeft-paddingRight-borderLeftWidth-borderRightWidth。如果是border-box,那么我们获取到的offsetwidth就已经包含了padding和border了,所以我们在argumentWithOrHeightt中什么也不做,直接返回offsetWidth值!
如果是通过css设置width/height,那么css中源码为:jQuery.style( elem, name, value ),所以最终调用的是jQuery.style,通过下面的jQuery.style还是调用height/width所在的cssHooks的相应的set方法。但是在style方法中extra是undefined。如css("width","100")那么

总之:设置width/height时候是多少就设置多少,如果是获取,那么content-box要用offsetWidth-2padding-2borderWidth,如果是boder-box那么是多少就是多少,因为在jQuery.style中还是通过style["width"]来进行赋值的!所以,赋值后如果是content-box那么下次获取会自动减去padding和border,也就是说这种赋值还是直接对内容部分的赋值!

	// Get and set the style property on a DOM Node
	style: function( elem, name, value, extra ) {
		// Don't set styles on text and comment nodes
		if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
			return;
		}
		// Make sure that we're working with the right name
		var ret, type, hooks,
			origName = jQuery.camelCase( name ),
			style = elem.style;
		name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) );

		// gets hook for the prefixed version
		// followed by the unprefixed version
		//获取到width/height对于的cssHooks!
		hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
		// Check if we're setting a value
		//我们在设置值!
		if ( value !== undefined ) {
			type = typeof value;
			// convert relative number strings (+= or -=) to relative numbers. #7345
			if ( type === "string" && (ret = rrelNum.exec( value )) ) {
				value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) );
				// Fixes bug #9237
				type = "number";
			}
			// Make sure that null and NaN values aren't set. See: #7116
			if ( value == null || value !== value ) {
				return;
			}
             //为我们的值添加px!
			// If a number was passed in, add 'px' to the (except for certain CSS properties)
			if ( type === "number" && !jQuery.cssNumber[ origName ] ) {
				value += "px";
			}
			// Fixes #8908, it can be done more correctly by specifing setters in cssHooks,
			// but it would mean to define eight (for every problematic property) identical functions
			if ( !support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) {
				style[ name ] = "inherit";
			}
			//我们在设置值的时候,如果hooks不存在,同时hooks中没有set方法,同时set方法返回的是undefind
			//那么我们直接调用style["name"]进行设置。如果有hook,同时有set,那么就调用set方法!
			// If a hook was provided, use that value, otherwise just set the specified value
			if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) {
				// Support: IE
				// Swallow errors from 'invalid' CSS values (#5509)
				try {
					style[ name ] = value;
				} catch(e) {}
			}
		} else {
			// If a hook was provided get the non-computed value from there
			if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {
				return ret;
			}

			// Otherwise just get the value from the style object
			return style[ name ];
		}
	},
width/height所在的cssHooks,这种调用方式是css("height/width")

jQuery.each([ "height", "width" ], function( i, name ) {
	//获取jQuery.cssHooks["height"],jQuery.cssHooks["width"]
	jQuery.cssHooks[ name ] = {
		get: function( elem, computed, extra ) {
			//传入了computed的cssStyleDeclaration
			if ( computed ) {
				// certain elements can have dimension info if we invisibly show them
				// however, it must have a current display style that would benefit from this
				//获取width,height时候要检测相应的Element的display,并且进行检测,不能是display-cell,display-caption
				//如果display满足的时候,同时offSetWidth是0那么调用swap函数,否则调用getWidthOrHeight
				return rdisplayswap.test( jQuery.css( elem, "display" ) ) && elem.offsetWidth === 0 ?
					//	cssShow = { position: "absolute", visibility: "hidden", display: "block" },
					jQuery.swap( elem, cssShow, function() {
						return getWidthOrHeight( elem, name, extra );
					}) :
						//elem元素,name
					getWidthOrHeight( elem, name, extra );
			}
		},
		set: function( elem, value, extra ) {
			var styles = extra && getStyles( elem );
			return setPositiveNumber( elem, value, extra ?
//调用set方法操作height/width时候,extra是false,所以调用setPositiveNumber(elem,value,0)
				augmentWidthOrHeight(
					elem,
					name,
					extra,
					support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
					styles
				) : 0
			);
		}
	};
});

setPositiveNumber源码:

function setPositiveNumber( elem, value, subtract ) {
	var matches = rnumsplit.exec( value );
           //rnumsplit = new RegExp( "^(" + pnum + ")(.*)$", "i" )
            //var pnum = (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source;
	return matches ?
		// Guard against undefined "subtract", e.g., when used as in cssHooks
		Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) :
		value;
}

getWidthOrHeight方法源码:

function getWidthOrHeight( elem, name, extra ) {
	// Start with offset property, which is equivalent to the border-box value
	var valueIsBorderBox = true,
		val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
		styles = getStyles( elem ),
		isBorderBox = support.boxSizing && 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
	if ( val <= 0 || val == null ) {
		// Fall back to computed then uncomputed css if necessary
		val = curCSS( elem, name, styles );
		//如果getComputedStyle没有获取到那么用style获取!
		if ( val < 0 || val == null ) {
			val = elem.style[ name ];
		}
		// Computed unit is not pixels. Stop here and return.
		if ( rnumnonpx.test(val) ) {
			return val;
		}
		// we need the check for style in case a browser which returns unreliable values
		// for getComputedStyle silently falls back to the reliable elem.style
		valueIsBorderBox = isBorderBox && ( support.boxSizingReliable() || val === elem.style[ name ] );
		val = parseFloat( val ) || 0;
	}
	// use the active box-sizing model to add/subtract irrelevant styles
	return ( val +
		augmentWidthOrHeight(
			elem,
			name,
			extra || ( isBorderBox ? "border" : "content" ),
			valueIsBorderBox,
			styles
		)
	) + "px";
}
argumentWidthOrHigth方法:

function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
	var i = extra === ( isBorderBox ? "border" : "content" ) ?
		// If we already have the right measurement, avoid augmentation
		4 :
		// Otherwise initialize for horizontal or vertical properties
		name === "width" ? 1 : 0,
		val = 0;
	for ( ; i < 4; i += 2 ) {
		// both box models exclude margin, so add it if we want it
		if ( extra === "margin" ) {
			val += jQuery.css( elem, extra + cssExpand[ i ], true, styles );
		}
       //var cssExpand = [ "Top", "Right", "Bottom", "Left" ];
		if ( isBorderBox ) {
			// border-box includes padding, so remove it if we want content
			//如果是content-box那么直接减去两个padding!
			if ( extra === "content" ) {
				val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
			}
			// at this point, extra isn't border nor margin, so remove border
			if ( extra !== "margin" ) {
				val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
			}
		} else {
			//如果不是border-box,那么添加padding值!
			// at this point, extra isn't content, so add padding
			val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
			// at this point, extra isn't content nor padding, so add border
			if ( extra !== "padding" ) {
				val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
			}
		}
	}
	return val;
}
注意:这两个方法的作用在于根据box-sizing的值获取到元素的相应属性!

看完上面通过css获取和设置height/width的方法以后,我们来仔细看看实例方法width/height,innerWidth/innerHeight,outerWidth/outerHeigth的源码:同时这一篇博客我们可以知道,获取值用的是jQuery.css( elem, type, extra )而设置值的时候用的是jQuery.style( elem, type, value, extra ); 其中extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );  efaultExtra是padding,content,""也就是说对于outerWidth/outerHeight来说,如果调用的时候如$("xx").outerWidth(true);或者是$("xxx").outerWidth("xx",true)那么就会是extrea=margin,如果没有传入任何一个true,那么extra就是border!

我们来看看获取值:

(1)如果是innerWidth/width那么调用的就是jQuery.css(elem,"width","padding/content"),其中padding/content表示如果是innerWidth那么就是padding否则就是content!因为这里的第二个参数是width,所以会获取到width对应的cssHooks,进而调用 hooks.get( elem, true, extra ),而extra就是padding/content->调用getWidthOrHeight(elem,name,"padding/content"),在这个方法中因为width明确有值,所以传入augmentWidthOrHeight就是"padding/content",如innerWidth传入的就是padding,最后就是减去borderLeftWith/boderRightWidth!对于width来说,那么传入的就是"content",最后就是减去border和padding两者!

(2)对于outerWith来说,如果用户明确传入了true,那么就是传入margin,在cssHooks["width"]中转化为get(elem,true,"margin"),最后的getHeightOrWidth中传入的是extra是margin,最后在argumentWithOrHeight中就是添加了margin值,也就是用了offsetWidth+margin(其中offsetWith在getHeightOrWidth中获取到)。如果用户没有明确指定true,那么传入css方法就是border,在css方法中转化为hooks.get(elem,true,"border"),getWidthOrHeight(elem,name,"border"),所以什么也不做!

(3)总结:对于innerWidth/width方法来说,innerWidth=offsetWidth-2borderWidth;width=offsetWidth-2padding-2borderWidth;对于outerWidth=offsetWidth+margin(用户传入了true)/offsetWidth(没有传入true)。注意:这个公式对于content-box/border-box都是成立的!

下面我们来看看设置值:

(1)设置值就是通过把传入的参数加上特定的尺寸完成的,最终通过style["width"]来完成的。所以在border-box下,调用innerWidth只要把参数加上一个2border作用到style中就能够完成!该过程通过setPositiveNumber完成。

 content-box border-box
innerWidth()              width+2padding width-2border
width()                          width width-2border-2padding
outerWidth(true)       width+2padding+2border+margin  width+margin
style.width                  width  width

opacity所在的Hooks:

	jQuery.cssHooks.opacity = {
		get: function( elem, computed ) {
			// IE uses filters for opacity
			return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ?
				( 0.01 * parseFloat( RegExp.$1 ) ) + "" :
				computed ? "1" : "";
		},
		set: function( elem, value ) {
			var style = elem.style,
				currentStyle = elem.currentStyle,
				opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "",
				filter = currentStyle && currentStyle.filter || style.filter || "";
			// IE has trouble with opacity if it does not have layout
			// Force it by setting the zoom level
			style.zoom = 1;
			// if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652
			// if value === "", then remove inline opacity #12685
			if ( ( value >= 1 || value === "" ) &&
					jQuery.trim( filter.replace( ralpha, "" ) ) === "" &&
					style.removeAttribute ) {
				// Setting style.filter to null, "" & " " still leave "filter:" in the cssText
				// if "filter:" is present at all, clearType is disabled, we want to avoid this
				// style.removeAttribute is IE Only, but so apparently is this code path...
				style.removeAttribute( "filter" );

				// if there is no filter style applied in a css rule or unset inline opacity, we are done
				if ( value === "" || currentStyle && !currentStyle.filter ) {
					return;
				}
			}
			// otherwise, set new filter values
			style.filter = ralpha.test( filter ) ?
				filter.replace( ralpha, opacity ) :
				filter + " " + opacity;
		}
	};
}

marginRight所在的Hooks:

目地:在老版本的webkit中修改width会影响marginRight为width的值。如果为true表示不会影响。于是我们把整个关于marginRight的hook移除,因为当前浏览器中不会影响,下次就不用检测了!如果会影响marginRight的值,那么我们把获取marginRight的值修改为另外一个函数,也就是我们这里指定的函数。在该函数里面我们首先把该元素的display设置为inline-block,然后获取该元素的marginRight的值!

jQuery.cssHooks.marginRight = addGetHookIf( support.reliableMarginRight,
	function( elem, computed ) {
		if ( computed ) {
			// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
			// Work around by temporarily setting element display to inline-block
			return jQuery.swap( elem, { "display": "inline-block" },
				curCSS, [ elem, "marginRight" ] );
		}
	}
);

函数addGetHookIf

目地:兼容不同的浏览器给出不同的处理函数!如果浏览器支持那么就直接移除这个返回的对象,如果不支持那么就设置相应的回调函数!

function addGetHookIf( conditionFn, hookFn ) {
	// Define the hook, we'll check on the first run if it's really needed.
	return {
		get: function() {
			var condition = conditionFn();
			if ( condition == null ) {
				// The test was not ready at this point; screw the hook this time
				// but check again when needed next time.
				return;
			}
			if ( condition ) {
				// Hook not needed (or it's not possible to use it due to missing dependency),
				// remove it.
				// Since there are no other hooks for marginRight, remove the whole object.
				delete this.get;
				return;
			}
			// Hook needed; redefine it so that the support test is not executed again.
			return (this.get = hookFn).apply( this, arguments );
		}
	};
}

margin,padding,borderWidth所在的Hooks:

jQuery.each({
	margin: "",
	padding: "",
	border: "Width"
}, function( prefix, suffix ) {
	jQuery.cssHooks[ prefix + suffix ] = {
		expand: function( value ) {
			var i = 0,
				expanded = {},
				// assumes a single number if not a string
				parts = typeof value === "string" ? value.split(" ") : [ value ];
			for ( ; i < 4; i++ ) {
				expanded[ prefix + cssExpand[ i ] + suffix ] =
					parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
			}
			return expanded;
		}
	};
	if ( !rmargin.test( prefix ) ) {
		jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
	}
});
swap函数源码:

jQuery.swap = function( elem, options, callback, args ) {
	var ret, name,
		old = {};
	// Remember the old values, and insert the new ones
	for ( name in options ) {
		old[ name ] = elem.style[ name ];
		elem.style[ name ] = options[ name ];
	}
   //把该元素的position设为absolute,visibility设为hidden,把display设为block
   //然后再次获取到这个table-cell,table-caption同时offsetWidth为0的元素额参数
	ret = callback.apply( elem, args || [] );
   //然后给该元素恢复原来的值
	// Revert the old values
	for ( name in options ) {
		elem.style[ name ] = old[ name ];
	}

	return ret;
};
swap函数总结:

(1)该函数的作用在于把元素的相应的属性设置为新的属性然后调用指定的函数,然后把新的属性值恢复为旧的属性值!

(2)我们必须弄清楚,如果把属性值设置为新的值以后要么就会发生重绘要么就会发生重排版!(如设置属性后调用getComputedStyle)

下面我想给出几个读源码中的几个问题和解决方案:

getBoundingClientRect测试代码1:

HTML部分:

<body style="background-color:yellow;">
<div style="margin:10px;width:100px;border:1px solid red;height:100px;">
</div>
</body>
JS部分

//打印18=8(body默认的margin是8px)+10
alert($("div")[0].getBoundingClientRect().left);
//打印8+10+1+100+1=120不算右边的margin,只是到了border-right就结束了!
alert($("div")[0].getBoundingClientRect().right);
//打印10px
alert($("div")[0].getBoundingClientRect().top);
//打印112=10(上下margin被合并了,8和10选择10px)+1×2+100=112
alert($("div")[0].getBoundingClientRect().bottom);
总结:

(1)getBoundingClientRect方法会穿过margin一直找到元素内容的边界,也就是border距离文档的left,right,top,bottom的距离,记住这里一直到border!

(2)文章中牵涉到一个重要的概念BFC,也就是块级格式化上下文,如果给body设置一个border:1px solid red;那么border的margin和div的margin就不会合并,那么top的值就是8+1+10=19px。这里你要弄懂上下margin合并的概念!

(3)可以通过window.getComputedStyle知道border的margin默认是8px,这是为什么以前老是用*{margin:0px;padding:0px;}来css reset!
getBoundingClientRect测试代码2:

         HTML部分:

</pre><pre name="code" class="html"><body style="width:1000px;">
<div id="parent"  style="border:1px solid red;position:absolute;">
	<div id="child" style="position:relative;padding-top:10px;">
         <div id="grandChild" style="width:200px;padding:10px;border:1px solid red;margin:10px;">
			这里是孙子!
		</div>
	</div>
</div>
</body>
JS部分:

//要知道offset的属性如offsetWidth,offsetHeight都是包括边框的!所以offsetTop也是从边框外开始计算的!所以这里的grandChild
//元素要相对于offsetParent定位获取到offsetTop(这里的offsetParent是child元素),是不包括边框的,所以不管为grandChild的
//border是多少,结果打印都是0!如果你给grandChild添加一个margin-top为10px那么他的offsetTop还是0,因为offsetTop讲究的
//是整个盒子!但是如果你把child设置一个padding-top:10px那么就会变成10px,因为我的盒子和外面的offsetParent的距离就是10px了!
alert($("#grandChild")[0].offsetTop);
//就是border的宽度!
alert($("#grandChild")[0].clientLeft);
//所以body的margin默认会有8px,但是padding是0,这是为什么要css reset!
alert(window.getComputedStyle($("body")[0],"")["margin"]);
//默认是8
alert($("body")[0].getBoundingClientRect().top);
//默认是8
alert($("body")[0].getBoundingClientRect().left);
//打印29,(因为getBoundingClientRect会一直获取到元素的border,border才是元素的边界),8(border的margin)+1(parent的border)+10(child的paddingTop)+10(grandChild的marginTop)
alert($("#grandChild")[0].getBoundingClientRect().top);
//打印19px,(因为getBoundingClientRect会一直获取到元素的border,border才是元素的边界),8(border的margin)+1(parent的border)+10(grandChild的marginLeft)
alert($("#grandChild")[0].getBoundingClientRect().left);
//8(body的margin)+1(parent的border)+10(grandChild的marginLeft)+1(grandChild的borderLeft)+10(grandChild的paddingLeft)+200(grandChild的width)+10(grandChild的paddingRight)+1(grandChild的borderRight)打印241
alert($("#grandChild")[0].getBoundingClientRect().right);
//通过最后上面两个left和right的值相减你应该有所体会,left为19,right为241,241-19=222=10(grandChild的marginLeft)+1(borerLeft)+200(width)+1(borderRight)+10(marginRight)
//也就是整个盒子的大小!(包括margin等)
总结:

(1)offsetTop讲究的整个盒子包括margin等相对于父元素的距离。

(2)clientLeft,clientTop等是盒子模型的border的宽度!
(3)getBoundingClientRect是不算自己的盒子的外部margin的值的,也就是在算marginTop的时候会一直经过自己的盒子的margin的值一直到border为止表示的就是这个元素距离文档上端的距离!(但是offsetTop是包括自己的margin在内的整个盒子相对于父元素的位置!)

   总之一句话:offsetTop是包含margin边界,getBoundingClientRect是不包含margin边界的!
(4)getBoundingClientRect表示的是当前元素相对于视口的距离,所以要获取元素相对于文档的距离还要加上pageXOffset,为了兼容IE<8要减去documentElement.scrollTop!

(5)如果元素的position是fixed,那么获取到getBoundingClientRect就是元素的offset值,他的offsetParent是window,而window的left/top都是0,所以这时候要获取该元素距离offsetParent的距离只要是getBoundingClientRect.top-0-marginTop就行!因为offsetTop不包含margin边界所以要减去!

(5)这里还有一种新的用法可以学学,一般API没有(HTML代码见上面):

$("#content").offset({top:100,using:function(prop)
{
//alert(this.id);这里面的this指向了前面的调用对象的DOM对象!
//这里的prop表示我们要把前面的调用对象的DOM元素的style设置为多少!
//alert(prop.top);根据上面我提供的html代码,这里打印92表示应该把
//把调用对象的DOM元素的top设置为920x!注意:这里我只是传入了top!
//所以该对象的left为undefined!
 }})

你可能感兴趣的:(jQuery源码分析之offset,position,offsetParent方法以及源码中常见的cssHooks,swap代码)