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對應的集合.!
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!
}})