原计划是没有这篇博文的,研究animation源码的时候遇到了css样式这个拦路虎。比如jQuery支持“+=10”、“+=10px”定义一个属性的增量,但是有的属性设置时可以支持数字,有的必须有单位;在对属性当前值读取时,不同的浏览器可能返回不同的单位值,无法简单的相加处理;在能否读取高宽等位置信息上,还会受到display状态的影响;不同浏览器,相同功能对应的属性名不同,可能带有私有前缀等等。
众多的疑问,让我决定先破拆掉jQuery的样式机制。
(本文采用 1.12.0 版本进行讲解,用 #number 来标注行号)
jQuery实现了一套简单统一的样式读取与设置的机制。$(selector).css(prop)读取,$(selector).css(prop, value)写入,也支持对象参数、映射写入方式。厉害的是,这种简单高效的用法,完全不用考虑兼容性的问题,甚至包括那些需要加上前缀的css3属性。
/* 读取 */
$('#div1').css('lineHeight')
/* 写入 */
$('#div1').css('lineHeight', '30px')
// 映射(这种写法其实容易产生bug,不如下面一种,后文会讲到)
$('#div1').css('lineHeight', function(index, value) {
return (+value || 0) + '30px';
})
// 增量(只支持+、-,能够自动进行单位换算,正确累加)
$('#div1').css('lineHeight', '+=30px')
// 对象写法
$('#div1').css({
'lineHeight': '+=30px' ,
'fontSize': '24px'
})
如何统一一个具有众多兼容问题的系统呢?jQuery的思路是抽象一个标准化的流程,然后对每一个可能存在例外的地方安放钩子,对于需要例外的情形,只需外部定义对应的钩子即可调整执行过程,即标准化流程 + 钩子。
下面我们来逐个击破!
jQuery.fn.css( name, value )
的功能是对样式的读取和写入,属于外部使用的外观方法。内部的核心方法是jQuery.css( elem, name, extra, styles )
、jQuery.style( elem, name, value, extra )
。
jq中链式调用、对象写法、映射、无value则查询这些特点套用在了很多API上,分成两类。比如第一类:jQuery.fn.css(name, value)、第二类:jQuery.fn.html(value),第二类不支持对象参数写法。jq抽离了不变的逻辑,抽象成了access( elems, fn, key, value, chainable, emptyGet, raw )
入口。
难点(怪异的第二类)
第一类(有key,bulk = false)
普通(raw):对elems(一个jq对象)每一项->fn(elems[i], key, value)
映射(!raw):对elems每一项elems[i],求得key属性值val=fn(elems[i], key),执行map(即value)函数value.call( elems[ i ], i, val )得到返回值re,执行fn(elems[i], key, re)
取值(!value):仅取第一项fn( elems[ 0 ], key )
第二类(无key,bulk = true)
普通(raw):直接fn.call(elems, value)
映射(!raw):对elems每一项elems[i],求得值val=fn.call( jQuery( elems[i] )),执行map(即value)函数value.call( jQuery(elems[ i ]), val )得到返回值re,执行fn.call( jQuery( elems[i] ), re)
取值(!value):取fn.call( elems )
正是这两类的不同造成了access内部逻辑的难懂,下面代码中第二类进行fn封装,就是为了bulk->!raw->map能够与第一类使用同样的逻辑。两类的使用方法,包括映射参数写法都是不同的。有value均为链式调用chainable=true
// #4376
var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
var i = 0,
length = elems.length,
// 确定哪一类,false为第一类
bulk = key == null;
// 第一类对象写法,为设置,开启链式
if ( jQuery.type( key ) === "object" ) {
chainable = true;
// 拆分成key-value式写法,静待链式返回
for ( i in key ) {
access( elems, fn, i, key[ i ], true, emptyGet, raw );
}
// value有值,为设置,开启链式
} else if ( value !== undefined ) {
chainable = true;
if ( !jQuery.isFunction( value ) ) {
raw = true;
}
if ( bulk ) {
// bulk->raw 第二类普通赋值,静待链式返回
if ( raw ) {
fn.call( elems, value );
fn = null;
// bulk->!raw 第二类map赋值,封装,以便能使用第一类的式子
} else {
bulk = fn;
fn = function( elem, key, value ) {
return bulk.call( jQuery( elem ), value );
};
}
}
if ( fn ) {
for ( ; i < length; i++ ) {
// 第一类raw普通,!raw映射。封装后的第二类共用映射方法
fn(
elems[ i ],
key,
raw ? value : value.call( elems[ i ], i, fn( elems[ i ], key ) )
);
}
}
}
return chainable ?
// 赋值,链式
elems :
// 取值
bulk ?
// 第二类
fn.call( elems ) :
// 第一类
length ? fn( elems[ 0 ], key ) : emptyGet;
};
读取依赖window上的getComputedStyle方法,IE6-8依赖元素的currentStyle方法。样式的写入依赖elem.style。
jQuery.fn.css( name, value )为什么会有两个核心方法呢?因为样式的读取和写入不是同一个方式,而写入的方式有时候也会用来读取。
读:依赖window上的getComputedStyle方法,IE6-8依赖元素的currentStyle方法。内联外嵌的样式都可查到
写:依赖elem.style的方式。而elem.style方式也可以用来查询的,但是只能查到内联的样式
因此封装了两个方法jQuery.css( elem, name, extra, styles )
、jQuery.style( elem, name, value, extra )
,前者只读,后者可读可写,但是后者的读比较鸡肋,返回值可能出现各种单位,而且还无法查到外嵌样式,因此jQuery.fn.css方法中使用前者的读,后者的写
// #7339
jQuery.fn.css = function( name, value ) {
// access第一类用法,fn为核心函数的封装,直接看返回值
return access( this, function( elem, name, value ) {
var styles, len,
map = {},
i = 0;
// 增加一种个性化的 取值 方式。属性数组,返回key-value对象
// 第一类取值,只取elems[0]对应fn执行的返回值
if ( jQuery.isArray( name ) ) {
styles = getStyles( elem );
len = name.length;
for ( ; i < len; i++ ) {
// false参数则只返回未经处理的样式值,给定了styles则从styles对象取样式
// 下面会单独讲jQuery.css
map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
}
return map;
}
return value !== undefined ?
// 赋值
jQuery.style( elem, name, value ) :
// 取值
jQuery.css( elem, name );
}, name, value, arguments.length > 1 );
};
第一步:jq支持驼峰和’-‘串联两种写法,会在核心方法中统一转化为小驼峰形式
第二步:不同浏览器的不同属性名兼容,如float为保留字,标准属性是cssFloat,IE中使用styleFloat(当前版本IE已抛弃)。查看是否在例外目录中
第三步:css3属性支持程度不一,有的需要加上私有前缀才可使用。若加上私有前缀才能用,添加到例外目录中方便下次拿取
// #83,#356,第一步,小驼峰
rmsPrefix = /^-ms-/,
rdashAlpha = /-([\da-z])/gi,
fcamelCase = function( all, letter ) {
return letter.toUpperCase();
};
// #356
camelCase = function( string ) {
return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
},
// #7083,第二步
cssProps: {
// normalize float css property
"float": support.cssFloat ? "cssFloat" : "styleFloat"
}
// #6854,第三步
cssPrefixes = [ "Webkit", "O", "Moz", "ms" ],
emptyStyle = document.createElement( "div" ).style;
// return a css property mapped to a potentially vendor prefixed property
function vendorPropName( name ) {
// 查询无前缀驼峰名是否支持
if ( name in emptyStyle ) {
return name;
}
// 首字母变为大写
var capName = name.charAt( 0 ).toUpperCase() + name.slice( 1 ),
i = cssPrefixes.length;
// 查找是否有支持的私有前缀属性
while ( i-- ) {
name = cssPrefixes[ i ] + capName;
if ( name in emptyStyle ) {
return name;
}
}
}
jq的样式机制之所有复杂,因为在核心方法功能的设计上,考虑了非必要的“易用”、“兼容”、“扩展”。使得设置更灵活、输出更一致、支持累加换算、支持拓展的功能。由于对下面特性的支持,因此对样式的取值抽象出了核心逻辑curCSS( elem, name, computed )
,一来是逻辑划分更清晰,内部更可根据需要酌情选择使用这两者
易用
1、为了提高易用性,jQuery.style()可以自动为设置值加上默认单位’px’,由于有些属性值可以为数字,因此定义了cssNumber
的列表,列表中的项目不会加上默认单位。
2、允许增量’+=20px’式写法,由于采用jQuery.css获取的初始值单位有可能不同,因此封装了一个自动单位换算并输出增量后最终结果的函数adjustCSS()
兼容
并不是每个属性都能返回预期的值。
1、比如opacity在IE低版本是filter,用jQuery.style方式取值时需要匹配其中数字,结果跟opacity有100倍差距,而且设置的时候alpha(opacity=num)的形式也太独特。
2、比如定位信息会因为元素display为none等状态无法正确获取到getBoundingClientRect()、offsetLeft、offsetWidth等位置信息及大小,而且对于自适应宽度无法取得宽高信息。
3、比如一些浏览器兼容问题,导致某些元素返回百分比等非预期值。
jQuery.cssHooks
是样式机制的钩子系统。可以对需要hack的属性,添加钩子,在jQuery.css、jQuery.style读取写入之前,都会先看是否存在钩子并调用,然后决定是否继续下一步还是直接返回。通过在set
、get
属性中定义函数,使得行为正确一致。
扩展
1、返回值:jQuery.css对样式值的读取,可以指定对于带单位字符串和”auto”等字符串如何返回,新增了extra
参数。为”“(不强制)和true(强制)返回去单位值,false不做特殊处理直接返回。
2、功能扩展:jq允许直接通过innerWidth()/innerHeight()、outerWidth()/outerHeight()读取,也支持赋值,直接调整到正确的宽高。这是通过extra指定padding、border、margin等字符串做到的
3、cssHooks.expand:对于margin、padding、borderWidth等符合属性,通过扩展expand接口,可以得到含有4个分属性值的对象。
bug
使用字符串’30’和数字30的效果有区别。对于不能设为数字的,数字30自动加上px,字符串的却不会。
下面adjustCSS换算函数中也提到一个bug,下面有描述。
建议:adjustCSS函数本身就可以处理增量和直接量两种情况,type===’string’判断的地方不要ret[ 1 ],以解决第一个问题。adjustCSS返回一个数组,第一个为值,第二个为单位,这样就防止第二个bug。
// #4297,pnum匹配数字,rcssNum -> [匹配项,加/减,数字,单位]
var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source;
var rcssNum = new RegExp( "^(?:([+-])=)(" + pnum + ")([a-z%]*)$", "i" );
// #6851
cssNormalTransform = {
letterSpacing: "0",
fontWeight: "400"
}
// #7090,核心方法
jQuery.extend( {
// 支持数字参数的属性列表,不会智能添加单位
cssNumber: {
"animationIterationCount": true,
"columnCount": true,
"fillOpacity": true,
"flexGrow": true,
"flexShrink": true,
"fontWeight": true,
"lineHeight": true,
"opacity": true,
"order": true,
"orphans": true,
"widows": true,
"zIndex": true,
"zoom": true
},
// elem.style方式读写
style: function( elem, name, value, extra ) {
// elem为文本和注释节点直接返回
if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
return;
}
/**
* ---- 1、name修正,属性兼容 ----
*/
var ret, type, hooks,
// 小驼峰
origName = jQuery.camelCase( name ),
style = elem.style;
// 例外目录、私有前缀
name = jQuery.cssProps[ origName ] ||
( jQuery.cssProps[ origName ] = vendorPropName( origName ) || origName );
// 钩子
// 先name、后origName使钩子更灵活,既可统一,又可单独
hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
/**
* ---- 2、elem.style方式 - 赋值 ----
*/
if ( value !== undefined ) {
type = typeof value;
// '+='、'-='增量运算
// Convert "+=" or "-=" to relative numbers (#7345)
if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {
// adjustCSS对初始值和value进行单位换算,相加/减得到最终值(数值)
value = adjustCSS( elem, name, ret );
// 数值需要在下面加上合适的单位
type = "number";
}
// Make sure that null and NaN values aren't set. See: #7116
if ( value == null || value !== value ) {
return;
}
// 数值和'+=xx'转换的数值,都需要加上单位。cssNumber记录了可以是数字的属性,否则默认px
// ret[3]为'+=xx'原本匹配的单位
if ( type === "number" ) {
value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "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";
}
// 有钩子先使用钩子,看返回值是否为undefined决定是否style[ name ]赋值,否则直接赋值
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 ) {}
}
/**
* ---- 3、elem.style方式 - 取值 ----
*/
} else {
// 有钩子先使用钩子,看返回值是否为undefined决定是否style[ name ]取值,否则直接取值
if ( hooks && "get" in hooks &&
( ret = hooks.get( elem, false, extra ) ) !== undefined ) {
return ret;
}
return style[ name ];
}
},
// 默认computedStyle/currentStyle方式只读,也可styles指定读取对象
css: function( elem, name, extra, styles ) {
/**
* ---- 1、name修正,属性兼容(同style) ----
*/
var num, val, hooks,
origName = jQuery.camelCase( name );
name = jQuery.cssProps[ origName ] ||
( jQuery.cssProps[ origName ] = vendorPropName( origName ) || origName );
// 钩子
hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
// 若有钩子,通过钩子读取
if ( hooks && "get" in hooks ) {
val = hooks.get( elem, true, extra );
}
// 没有钩子,通过封装的curCSS读取
if ( val === undefined ) {
val = curCSS( elem, name, styles );
}
// 属性值为"normal",若为cssNormalTransform内的属性,把对应值输出
if ( val === "normal" && name in cssNormalTransform ) {
val = cssNormalTransform[ name ];
}
// extra === "" 去单位处理,若为"normal"、"auto"等字符串,原样返回
// extra === true 强制去单位,若为parseFloat后为NaN的字符串,返回0
// extra === false/undefined 不特殊处理
if ( extra === "" || extra ) {
num = parseFloat( val );
return extra === true || isFinite( num ) ? num || 0 : val;
}
return val;
}
} );
adjustCSS( elem, prop, valueParts, tween )
用于调用jQuery.style对增量计算的换算,并得到最终值。在jq内部,除了css样式会换算,动画处理也支持换算。这里也可以把动画的tween对象的初始值和增量进行累加换算,得到最终值赋给tween对象
难点:
这里需要知道jQuery.css( elem, prop, "" )
通过computedStyle/currentStyle求得的值单位不变,并且被extra=”“去掉了单位。比如初始值是30px,增量为’+=1rem’,先使用增量的单位30rem,然后调用jQuery.css查询跟修改前的初始值比较,比如变成了scale=15倍,则30rem/15=2rem求得原值换算后为2rem,然后再累加返回3rem。
maxIterations设为20有两个原因:1、js浮点误差可能导致两边总是不相等;2、对于首次调整单位变成了很小的倍数趋近于0无法计算,则通过重置为0.5每次乘2直到可以计算,慢慢的调整差距
bug(建议见第4点):
cssNumber列表中属性的值使用无单位增量如’+=10’,而初始值单位为px,将按照初始值单位’+=10px’处理后返回。但返回到外部,由于在cssNumber列表中,并不会再次加上单位,按照倍数被设置了。
比如lineHeight初始值20px,使用’+=4’,变成了赋值24倍
// valueParts为增量匹配结果集,两种形式
// 动画 adjustCSS(tween.elem, prop, rcssNum.exec( value ), tween)
// css adjustCSS(elem, prop, rcssNum.exec( value ))
function adjustCSS( elem, prop, valueParts, tween ) {
var adjusted,
// 默认比例
scale = 1,
// 最大修正次数
maxIterations = 20,
currentValue = tween ?
// 动画对象当前属性值计算
function() { return tween.cur(); } :
function() { return jQuery.css( elem, prop, "" ); },
// 当前用作累加基数的初始值
initial = currentValue(),
// 匹配单位,若不在cssNumber目录,并且没带单位,则当做px
unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),
// 由于初始值若匹配到单位,都会是px,不是的在执行css过程中jq也有钩子修正,所以有可能需要换算的只有cssNumber列表中项目,或者unit不为px且initial有非0数值的(0无需换算)。初始值为字符串如"auto",则会在下面按照0处理
initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) &&
rcssNum.exec( jQuery.css( elem, prop ) );
// 单位不同时换算
if ( initialInUnit && initialInUnit[ 3 ] !== unit ) {
// 默认使用增量值单位
// 若为cssNumber中属性,且增量无单位,则使用初始值单位,后面也无需换算了
// 小bug:cssNumber列表中属性'+=10'若无unit,按照初始值单位'+=10px'处理返回。但返回到外部,由于在cssNumber列表中,并不会再次加上单位,按照倍数被设置了。比如lineHeight初始值20px,使用'+=4',变成了赋值24倍
unit = unit || initialInUnit[ 3 ];
// Make sure we update the tween properties later on
valueParts = valueParts || [];
// Iteratively approximate from a nonzero starting point
// 此处个人觉得没有 || 1 的写法没有必要性,若为0,则无需换算了
initialInUnit = +initial || 1;
// 换算,见难点解释
do {
// If previous iteration zeroed out, double until we get *something*.
// Use string for doubling so we don't accidentally see scale as unchanged below
scale = scale || ".5";
// Adjust and apply
initialInUnit = initialInUnit / scale;
jQuery.style( elem, prop, initialInUnit + unit );
// Update scale, tolerating zero or NaN from tween.cur()
// Break the loop if scale is unchanged or perfect, or if we've just had enough.
} while (
scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations
);
}
if ( valueParts ) {
// 初始值为字符串,也将按照0处理,需要注意咯
initialInUnit = +initialInUnit || +initial || 0;
// 根据是否为增量运算判断直接赋值还是换算后的初始值与增量相加,css运算中只允许增量运算使用该函数
adjusted = valueParts[ 1 ] ?
initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] :
+valueParts[ 2 ];
// 对动画对象赋初值和末值
if ( tween ) {
tween.unit = unit;
tween.start = initialInUnit;
tween.end = adjusted;
}
}
return adjusted;
}
curCSS( elem, name, computed )
是对getStyles( elem )
的封装,可以通过computed指定样式对象替代内部的getStyle。对高版本浏览器和低版本IE getStyle分别使用的getComputedStyle、currentStyle,前者是全局对象下的属性,所以源码中使用了ownerDocument.defaultView指代。
不同的浏览器,对属性的返回可能出现百分比等非px返回值,jq通过钩子处理个体,curCSS内部也处理了一些情况,比如Chrome、Safari的margin相关属性值返回百分比,低版本IE的非top等位置属性返回百分比等。
// #6489,下面会用到的正则
var rmargin = ( /^margin/ );
var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" );
// #6692
var getStyles, curCSS,
rposition = /^(top|right|bottom|left)$/;
// 高版本浏览器
if ( window.getComputedStyle ) {
getStyles = function( elem ) {
// Support: IE<=11+, Firefox<=30+ (#15098, #14150)
// IE throws on elements created in popups
// FF meanwhile throws on frame elements through "defaultView.getComputedStyle"
var view = elem.ownerDocument.defaultView;
// opener指的是打开该页面的源页面的window
if ( !view || !view.opener ) {
view = window;
}
return view.getComputedStyle( elem );
};
// 默认使用getStyles,也可通过computed参数指定样式对象。内部还有对文档片段和margin类属性值的特殊处理
curCSS = function( elem, name, computed ) {
var width, minWidth, maxWidth, ret,
style = elem.style;
computed = computed || getStyles( elem );
// getPropertyValue is only needed for .css('filter') in IE9, see #12537
// getComputedStyle(elem).getPropertyValue(name)其实也可以用来获取属性,但是不支持驼峰,必须-连接书写,否则返回""
ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined;
// 文档片段document fragments的元素通过getComputedStyle取样式是""或undefined,需要退回到style方式取
// 文档片段中的元素elem.ownerDocument不为文档片段,为docuemnt
if ( ( ret === "" || ret === undefined ) && !jQuery.contains( elem.ownerDocument, elem ) ) {
ret = jQuery.style( elem, name );
}
if ( computed ) {
// 为了兼容有的浏览器margin相关方法返回百分比等非px值的情况,由于width输出是px,并且margin的百分比是按照width计算的,因此可以直接赋值width。设置minWidth/maxWidth是为了保证设置的width不会因为超出限制失效
if ( !support.pixelMarginRight() && rnumnonpx.test( ret ) && rmargin.test( name ) ) {
// 记忆
width = style.width;
minWidth = style.minWidth;
maxWidth = style.maxWidth;
// 把margin的值设置到width,并获取对应width值作为结果
style.minWidth = style.maxWidth = style.width = ret;
ret = computed.width;
// 还原
style.width = width;
style.minWidth = minWidth;
style.maxWidth = maxWidth;
}
}
// Support: IE
// IE returns zIndex value as an integer.都以字符串返回
return ret === undefined ?
ret :
ret + "";
};
// IE 6-8
} else if ( documentElement.currentStyle ) {
getStyles = function( elem ) {
return elem.currentStyle;
};
curCSS = function( elem, name, computed ) {
var left, rs, rsLeft, ret,
style = elem.style;
computed = computed || getStyles( elem );
ret = computed ? computed[ name ] : undefined;
// Avoid setting ret to empty string here
// so we don't default to auto
if ( ret == null && style && style[ name ] ) {
ret = style[ name ];
}
// From the awesome hack by Dean Edwards
// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
// If we're not dealing with a regular pixel number
// but a number that has a weird ending, we need to convert it to pixels
// but not position css attributes, as those are
// proportional to the parent element instead
// and we can't measure the parent instead because it
// might trigger a "stacking dolls" problem
// 对非位置top|left|right|bottom返回的,先把left属性保存,然后把属性设置到left上,然后取出
if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) {
// 记忆
left = style.left;
// runtimeStyle是低版本IE中表示运行中样式,可读可写,优先级大于style设置的
rs = elem.runtimeStyle;
rsLeft = rs && rs.left;
// Put in the new values to get a computed value out
if ( rsLeft ) {
rs.left = elem.currentStyle.left;
}
// 对于百分比,是以父元素宽度为基准。而对于fontSize,设置到left的1rem大小则是固定的
style.left = name === "fontSize" ? "1em" : ret;
ret = style.pixelLeft + "px";
// 还原
style.left = left;
if ( rsLeft ) {
rs.left = rsLeft;
}
}
// Support: IE
// IE returns zIndex value as an integer.数字以字符串形式返回
return ret === undefined ?
ret :
ret + "" || "auto";
};
}
正如第4点提到的,cssHooks存在的主要目的是应对存取出现的不一致行为。jq用support对象放置测试后的兼容性信息,源码#6519 - #6690行有很多support对象的样式方面兼容测试,内部通过addGetHookIf( conditionFn, hookFn )
来绑定钩子。钩子有个computed参数,用于标记是jQuery.css/style哪个方法的读操作触发的,对应true/false
存:存的不一致只有一个
1、opacity透明度对于IE内部使用filter,需要设置为alpha(opacity=100*value)的形式
*、对于padding、borderWidth、height、width的存只是做了负数变为0的特殊处理。
取:取的不一致比较多
1、opacity的”“按”1”处理,对于需要使用filter的IE低版本也要hook
2、height、width的获取需要display不为none和带有table的任意值(除了table、table-cell、table-caption三样),因此提供了swap( elem, options, callback, args )
用于以指定属性状态调用函数取值,之后还原状态
3、marginLeft、marginRight对于不支持返回可靠值的浏览器做处理,marginRight在display为”inline-block”下取值,marginLeft通过getBoundingClientRect比对与marginLeft设置为0后的位置差得到
4、top、left中有可能返回百分比的浏览器,先取值,若不为px单位,则调用内部position方法计算top、left(但是此方法是相对有定位父集或html的,对于position为relative的是有bug的,个人建议源码中可以对relative的使用getBoundingClientRect比对处理)
扩展: innerWidth()/innerHeight()/outerWidth()/outerHeight()
盒模型默认是宽高不包括padding、border、margin。css3里有boxSizing属性,content-box|border-box|inherit分别代表 “不包括padding、border、margin” | “包含border和padding” | “继承”。
jq通过innerWidth()/innerHeight()可以直接查询/设置content-box区域的长宽;通过outerWidth()/outerHeight()可查询/设置为border-box区域的长宽,增加一个参数true,如([value, ]true),可查询/设置为border-box区域加上margin区域的总长宽。
jq仍然是设置height、width,不过它会进行换算。通过augmentWidthOrHeight( elem, name, extra, isBorderBox, styles )
计算增量(数值),通过getWidthOrHeight( elem, name, extra )
得到最终值(带px字符串)。通过extra
来指明按照content、padding、border、margin中哪一个级别。
注意:
cssHooks内若要得到自身属性的样式,不调用jQuery.css,而是直接调用curCSS,包括getWidthOrHeight内,因为curCSS是纯粹的取值,不会调用钩子造成死循环
/* #1307 contains
* 节点包含。后面经常用来验证是否为文档片段中的元素
---------------------------------------------------------------------- */
// /^[^{]+\{\s*\[native \w/ -> 匹配内部方法 'funtion xxx() { [native code] }'
hasCompare = rnative.test( docElem.compareDocumentPosition );
// 返回布尔值,true表示 b节点在a节点内/a文档的根节点内(节点相等为false)
// ie9+及其他浏览器支持compareDocumentPosition,ie6-8支持contains,比较老的safari都不支持使用下面的函数
contains = hasCompare || rnative.test( docElem.contains ) ?
function( a, b ) {
var adown = a.nodeType === 9 ? a.documentElement : a,
bup = b && b.parentNode;
return a === bup || !!( bup && bup.nodeType === 1 && (
adown.contains ?
adown.contains( bup ) :
// a.compareDocumentPosition( bup ) = 16表示 a包含b
a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
));
} :
function( a, b ) {
if ( b ) {
// 若不等于父节点,继续冒泡知道根节点
while ( (b = b.parentNode) ) {
if ( b === a ) {
return true;
}
}
}
return false;
};
/* #6494 swap
* elem的style在options状态下调用callback.apply(elem, args),然后改回原属性。返回查到的值
---------------------------------------------------------------------- */
var swap = function( elem, options, callback, args ) {
var ret, name,
old = {};
// 记录原有值,设定上新值
for ( name in options ) {
old[ name ] = elem.style[ name ];
elem.style[ name ] = options[ name ];
}
ret = callback.apply( elem, args || [] );
// 还原旧值
for ( name in options ) {
elem.style[ name ] = old[ name ];
}
return ret;
};
/* #6816 addGetHookIf
* conditionFn()执行后返回true,说明支持,不会绑定hookFn钩子
---------------------------------------------------------------------- */
function addGetHookIf( conditionFn, hookFn ) {
// Define the hook, we'll check on the first run if it's really needed.
// 预执行和懒加载的方式均可,源码选择了懒加载。第一次当做钩子执行调用时绑定真实钩子或删除
return {
get: function() {
if ( conditionFn() ) {
// 支持,无需钩子
delete this.get;
return;
}
// 需要钩子,定义为hookFn。即使是第一次也要执行一次
return ( this.get = hookFn ).apply( this, arguments );
}
};
}
/* #6935 setPositiveNumber
* 保证非负值,保留单位,subtract可以指定需要减去的值
---------------------------------------------------------------------- */
function setPositiveNumber( elem, value, subtract ) {
var matches = rnumsplit.exec( value );
return matches ?
// Guard against undefined "subtract", e.g., when used as in cssHooks
Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) :
value;
}
/* #6944 augmentWidthOrHeight
* 根据extra类型计算增量(相对于height/width取值),返回纯数值
* 注意:读取为增量,写入为减量
---------------------------------------------------------------------- */
function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
// border-box -> border, content-box -> content。无需修正为4
var i = extra === ( isBorderBox ? "border" : "content" ) ?
// If we already have the right measurement, avoid augmentation
4 :
// height: 0(top) 2(bottom) width: 1(right) 3(left)
// cssExpand = [ "Top", "Right", "Bottom", "Left"];
name === "width" ? 1 : 0,
val = 0;
for ( ; i < 4; i += 2 ) {
// border-box content-box 想变为margin级别都需要 + margin值
if ( extra === "margin" ) {
val += jQuery.css( elem, extra + cssExpand[ i ], true, styles );
}
if ( isBorderBox ) {
// border-box = content级别 + "padding" + "border"(下面那个)
if ( extra === "content" ) {
// true 表示强制去单位
val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
}
// border-box = padding级别 + "border"
if ( extra !== "margin" ) {
val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
}
} else {
// 逻辑能走到这里,说明一定不是content级别,否则 i = 4
// content-box 变为任意级别都要 + padding 。 true 表示强制去单位
val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
// 变为border级别、margin级别要 + border
if ( extra !== "padding" ) {
val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
}
}
}
return val;
}
/* #6988 getWidthOrHeight
* 用于读取,会根据extra加上augmentWidthOrHeight增量
---------------------------------------------------------------------- */
function getWidthOrHeight( elem, name, extra ) {
// getWidthOrHeight = contentBox级别值 + augmentWidthOrHeight增量
// 这里直接用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 = support.boxSizing &&
jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
// Support: IE11 only,全屏浏览下bug,不了解(逃
// In IE 11 fullscreen elements inside of an iframe have
// 100x too small dimensions (gh-1764).
if ( document.msFullscreenElement && window.top !== window ) {
// Support: IE11 only
// Running getBoundingClientRect on a disconnected node
// in IE throws an error.
if ( elem.getClientRects().length ) {
val = Math.round( elem.getBoundingClientRect()[ name ] * 100 );
}
}
// 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 ) {
// 直接获取 width/height 作为基础值,若之后调用elem.style,说明support.boxSizingReliable()一定为false
val = curCSS( elem, name, styles );
if ( val < 0 || val == null ) {
val = elem.style[ name ];
}
// 匹配到非px且带单位的值,则直接退出
if ( rnumnonpx.test( val ) ) {
return val;
}
// 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";
}
/* #7201 cssHooks[ "height", "width" ]
* 防止设置负数。支持根据extra指定级别修正设定/获取值
---------------------------------------------------------------------- */
jQuery.each( [ "height", "width" ], function( i, name ) {
jQuery.cssHooks[ name ] = {
get: function( elem, computed, extra ) {
// innerWidth等API内部只调用jQuery.css,style方式不用钩子,所以false则退出
if ( computed ) {
// rdisplayswap = /^(none|table(?!-c[ea]).+)/
// cssShow = { position: "absolute", visibility: "hidden", display: "block" }
// display影响了定位信息的获取,比如offsetWidth为0。先设置cssShow属性获取到值,然后改回属性
return rdisplayswap.test( jQuery.css( elem, "display" ) ) &&
elem.offsetWidth === 0 ?
swap( elem, cssShow, function() {
return getWidthOrHeight( elem, name, extra );
} ) :
getWidthOrHeight( elem, name, extra );
}
},
set: function( elem, value, extra ) {
var styles = extra && getStyles( elem );
// 设置非负值,在设置时增量即为减量,第三个参数对于substract参数
return setPositiveNumber( elem, value, extra ?
augmentWidthOrHeight(
elem,
name,
extra,
support.boxSizing &&
jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
styles
) : 0
);
}
};
} );
/* #7053 #7233 cssHooks[ "opacity" ]
* 根据是否支持opacity,判断内部使用opacity还是filter
---------------------------------------------------------------------- */
cssHooks: {
opacity: {
get: function( elem, computed ) {
// elem.style['opacity']调用无需钩子,所以false则不处理
if ( computed ) {
// We should always get a number back from opacity
var ret = curCSS( elem, "opacity" );
return ret === "" ? "1" : ret;
}
}
}
},
// #7233 filter部分
if ( !support.opacity ) {
jQuery.cssHooks.opacity = {
// computed -> css(true)、style(false) 均hook
get: function( elem, computed ) {
// IE uses filters for opacity
// ropacity = /opacity\s*=\s*([^)]*)/i,
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
// ralpha = /alpha\([^)]*\)/i
style.filter = ralpha.test( filter ) ?
filter.replace( ralpha, opacity ) :
filter + " " + opacity;
}
};
}
/* #7282 cssHooks[ "marginRight" "marginLeft" ]
* 只对不支持的浏览器使用 get 钩子,通过addGetHookIf
---------------------------------------------------------------------- */
jQuery.cssHooks.marginRight = addGetHookIf( support.reliableMarginRight,
function( elem, computed ) {
// 仅css方式
if ( computed ) {
return swap( elem, { "display": "inline-block" },
curCSS, [ elem, "marginRight" ] );
}
}
);
jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,
function( elem, computed ) {
// 仅css方式
if ( computed ) {
return (
parseFloat( curCSS( elem, "marginLeft" ) ) ||
// Support: IE<=11+
// Running getBoundingClientRect on a disconnected node in IE throws an error
// Support: IE8 only
// getClientRects() errors on disconnected elems
( jQuery.contains( elem.ownerDocument, elem ) ?
// 与marginLeft=0的left坐标比对差值
elem.getBoundingClientRect().left -
swap( elem, { marginLeft: 0 }, function() {
return elem.getBoundingClientRect().left;
} ) :
// 文档片段中的元素按0处理
0
)
) + "px";
}
}
);
/* #10882 cssHooks[ "top" "left" ]
* 只对不支持px返回位置的浏览器使用 get 钩子,通过addGetHookIf
---------------------------------------------------------------------- */
//
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
// position是相对最近的有定位的祖先节点的偏移,对于position:relative是不合适的,算是个bug吧
return rnumnonpx.test( computed ) ?
jQuery( elem ).position()[ prop ] + "px" :
computed;
}
}
);
} );
第7点已经讲了很多,增量的处理,并不在这几个函数中,统一交给了augmentWidthOrHeight,使得逻辑变得简单统一,很好的实践了分离可变与不变的思想。
注意:它们不受jQuery.style中的小bug(如200有效,’200’无效)影响。它们不在cssNumber列表,虽然不会再该函数里为字符串自动补px,但是钩子中的setPositiveNumber是自动补单位px输出的。
// #269
jQuery.isWindow = function( obj ) {
/* jshint eqeqeq: false */
return obj != null && obj == obj.window;
};
// #10696
function getWindow( elem ) {
return jQuery.isWindow( elem ) ?
elem :
elem.nodeType === 9 ?
elem.defaultView || elem.parentWindow :
false;
}
// #10893
jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name },
// funcName代表innerWidth、innerHeight、outerWidth、outerHeight
function( defaultExtra, funcName ) {
// 参数 ( [ margin [,value] ] ),margin才是数值
// 正确使用 $().outerHeight(值, true)
jQuery.fn[ funcName ] = function( margin, value ) {
// defaultExtra有值,说明是padding和content的情况,只要有参数说明链式
// defaultExtra为"",说明是margin和border情况,只有首参数有值并不为boolean,则链式
var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
// 确定级别
extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
// 第1类access用法
return access( this, function( elem, type, value ) {
var doc;
if ( jQuery.isWindow( elem ) ) {
// "clientHeight" "clientWidth" 可视区大小
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
// unfortunately, this causes bug #3838 in IE6/8 only,
// but there is currently no good, small way to fix it.
return Math.max(
elem.body[ "scroll" + name ], doc[ "scroll" + name ],
elem.body[ "offset" + name ], doc[ "offset" + name ],
doc[ "client" + name ]
);
}
// 此处value -> chainable ? margin : undefined
return value === undefined ?
// extra传递级别,并且不强制的去除单位
jQuery.css( elem, type, extra ) :
// Set width or height on the element
jQuery.style( elem, type, value, extra );
}, type, chainable ? margin : undefined, chainable, null );
};
} );
} );
与jQuery.fn.css()在一起定义的还有jQuery.fn.show/hide/toggle()方法,通过display:none影响元素的可见性。如同swap函数会记录样式并还原一样,为了让隐藏还原为可见的过程中display与隐藏前保持一致,把display的原状态存入jQuery._data(elem, “olddisplay”)缓存,对于没有缓存的显示的时候会调用defaultDisplay( nodeName )
首先查询elemdisplay列表中是否有默认值,若没有,内部调用actualDisplay( name, doc )
通过创建一个相同标签检测display得到需要设置的display值(结果会添加到elemdisplay列表加快以后查询),从而保证正确的显示状态。这一切的核心过程就是showHide()
showHide()这个函数跟jq中很多函数一样,很污!合并大量分支条件,就是不肯多写两行代码的臭毛病,原谅我年轻,赶脚是在炫技!!
要点:
1、设置为hide,都是通过elem.style.display= none。缓存显示状态到_data(已经有缓存值则不需要)
2、当调整为显示时使用缓存值。缓存值一定是最终可靠值,尽管信任
3、这种逻辑关系建立在只通过showHide()机制控制是否显示,这样记忆的值才能真正代表上一次
show 变量代表变为显示状态,hidden 变量代表本身是隐藏
show(设置显示) -> hidden(本身隐藏) :调整为_data缓存中保存的用于显示的设置;若无缓存,先把display设置为”“,若此时检测仍为hidden,则设置display到元素初始默认,并缓存起来(对文档片段也是友好的)
show(设置显示) -> !hidden(本身显示) :不做处理。下面会路过elem.style.display === “” -> values[ index ] || “” ,相当于没处理
!show(设置隐藏)-> hidden(本身隐藏) :display && display !== “none”,不为”“但仍隐藏,说明这段代码是为了兼容文档片段的,会缓存elem.style.display。最终display设为none
!show(设置隐藏)-> !hidden(本身显示) :缓存css取到的生效样式,因为style的可能为”“,最终display设为none
/* #4304 isHidden
* 对display:none,或者文档片段中的节点,返回true
---------------------------------------------------------------------- */
var isHidden = function( elem, el ) {
elem = el || elem;
return jQuery.css( elem, "display" ) === "none" ||
!jQuery.contains( elem.ownerDocument, elem );
};
// #6427 默认display值列表
var iframe,
elemdisplay = {
// Support: Firefox
// We have to pre-define these values for FF (#10227)
HTML: "block",
BODY: "block"
};
/* #6443 actualDisplay
* 创建时默认生效的display
---------------------------------------------------------------------- */
function actualDisplay( name, doc ) {
var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ),
display = jQuery.css( elem[ 0 ], "display" );
// #6251 删除该节点
elem.detach();
return display;
}
/* #6459 defaultDisplay
* 创建时默认生效的display
---------------------------------------------------------------------- */
function defaultDisplay( nodeName ) {
var doc = document,
display = elemdisplay[ nodeName ];
if ( !display ) {
display = actualDisplay( nodeName, doc );
// If the simple way fails, read from inside an iframe
// 失效了,就在一个iframe再试一次
if ( display === "none" || !display ) {
// Use the already-created iframe if possible
iframe = ( iframe || jQuery( "" ) )
.appendTo( doc.documentElement );
// Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse
doc = ( iframe[ 0 ].contentWindow || iframe[ 0 ].contentDocument ).document;
// Support: IE
doc.write();
doc.close();
display = actualDisplay( nodeName, doc );
iframe.detach();
}
// Store the correct default display,缓存
elemdisplay[ nodeName ] = display;
}
return display;
}
/* #6878 showHide
* show为true设置显示,为false设置隐藏
---------------------------------------------------------------------- */
function showHide( elements, show ) {
var display, elem, hidden,
values = [],
index = 0,
length = elements.length;
for ( ; index < length; index++ ) {
elem = elements[ index ];
if ( !elem.style ) {
continue;
}
// 缓存值
values[ index ] = jQuery._data( elem, "olddisplay" );
display = elem.style.display;
if ( show ) {
// display === "none"是无需记忆的属性,可以直接修改来试探
if ( !values[ index ] && display === "none" ) {
elem.style.display = "";
}
// 显示转隐藏时缓存才必要,这里只是为了方便,因为要被设置为这个值,可以直接借用下面values[ index ] || ""
if ( elem.style.display === "" && isHidden( elem ) ) {
values[ index ] =
jQuery._data( elem, "olddisplay", defaultDisplay( elem.nodeName ) );
}
} else {
hidden = isHidden( elem );
// !hidden显示转!show隐藏需缓存,文档片段造成的隐藏仍需缓存display && display !== "none"
if ( display && display !== "none" || !hidden ) {
jQuery._data(
elem,
"olddisplay",
hidden ? display : jQuery.css( elem, "display" )
);
}
}
}
// 开始设置
for ( index = 0; index < length; index++ ) {
elem = elements[ index ];
if ( !elem.style ) {
continue;
}
if ( !show || elem.style.display === "none" || elem.style.display === "" ) {
elem.style.display = show ? values[ index ] || "" : "none";
}
}
return elements;
}
/* #7362 show hide toggle
* 外观,toggle不带参数则是设置为相反。带参数则控制show/hide
---------------------------------------------------------------------- */
jQuery.fn.extend( {
show: function() {
return showHide( this, true );
},
hide: function() {
return showHide( this );
},
toggle: function( state ) {
if ( typeof state === "boolean" ) {
return state ? this.show() : this.hide();
}
return this.each( function() {
// 文档片段总是隐藏,调用show
if ( isHidden( this ) ) {
jQuery( this ).show();
} else {
jQuery( this ).hide();
}
} );
}
} );
jQuery不仅能够便利的支持快捷的盒模型设置,还提供了兼容的方式获取定位信息。
jQuery.fn.offset()
无参数时,获取元素到页面左上角的距离,包括滚动条。通常通过getBoundingClientRect()获取相对视口的(对于IE6-8左上角左边从2,2开始算,所以要减去document.documentElement.clientTop/left),然后加上滚动距离window.pageXOffset/pageYOffset(IE低版本不支持,需要用document.documentElement.scrollTop/scrollLeft,这里不考虑IE混杂模式),下面还实现了jQuery.fn.scrollLeft()/scrollTop()
兼容的获取或设置元素或window的滚动距离,源码中没调用这个方法,个人觉得有点奇怪虽然无伤大雅。
支持两种参数形式(css获取的position为”static”会设为”relative”),内部调用jQuery.offset.setOffset( elem, options, i )
设置:
对象{left: x, top: x}直接设置
函数fn(i, options),会注入当前位置信息({left: x, top: x})到options,可在函数内修正坐标,之后options会被设置到元素
jQuery.fn.position()
读取基于最近有定位的祖先节点或根节点的border内侧的相对偏移,margin部分也算做了元素的一部分,所以偏移量不包含margin。之所以选择包含margin,是为了css设置top和left定位元素考虑,这样就不用担心因为margin的影响要修正top和left了
在原生方法中有elem.offsetLeft、elem.offsetTop,代表相对最近有定位祖先的距离,但是源码却没用它,反而用了更麻烦的方法获取到元素和有定位祖先的offset()然后相减,原因应该是因为IE低版本的两个方法有bug,获取的是相对父集的偏移而无需有定位。
/* #10698 show hide toggle
* document和window会返回对应的window,其他元素都返回false
---------------------------------------------------------------------- */
function getWindow( elem ) {
return jQuery.isWindow( elem ) ?
elem :
elem.nodeType === 9 ?
elem.defaultView || elem.parentWindow :
false;
}
/* #10706 jQuery.offset.setOffset
* jQuery.fn.offset内用于设置时调用的核心方法,设置top、left属性
---------------------------------------------------------------------- */
jQuery.offset = {
setOffset: function( elem, options, i ) {
var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,
position = jQuery.css( elem, "position" ),
curElem = jQuery( elem ),
props = {};
// 需要把默认变为相对定位,设置才能生效。此时top、left是以它当前位置为基准的(与absolute不同)
if ( position === "static" ) {
elem.style.position = "relative";
}
// 获取相对页面起始处的距离(含滚动区域)
curOffset = curElem.offset();
// 当前的top、left值
curCSSTop = jQuery.css( elem, "top" );
curCSSLeft = jQuery.css( elem, "left" );
// relative的auto是0,而absolute与fixed的auto并不相当于0,因为它们是相对最近的有定位祖先节点或根元素的
calculatePosition = ( position === "absolute" || position === "fixed" ) &&
jQuery.inArray( "auto", [ curCSSTop, curCSSLeft ] ) > -1;
// 计算
if ( calculatePosition ) {
// 为auto时,获取相对最近的有定位祖先节点或根元素的距离,得到真实值,得到值无单位。因为margin值被考虑在方法中,因此获取的top/left无需修正
curPosition = curElem.position();
curTop = curPosition.top;
curLeft = curPosition.left;
// relative无需计算,需要去除单位
} else {
curTop = parseFloat( curCSSTop ) || 0;
curLeft = parseFloat( curCSSLeft ) || 0;
}
// options为函数时,调用得到修正后需要设置的offset()坐标,并赋值给options
if ( jQuery.isFunction( options ) ) {
// fn(i, options) 可自定义修正坐标
options = options.call( elem, i, jQuery.extend( {}, curOffset ) );
}
// 相对之前offset()增加的值,就是需要在top\left属性上追加的值
if ( options.top != null ) {
props.top = ( options.top - curOffset.top ) + curTop;
}
if ( options.left != null ) {
props.left = ( options.left - curOffset.left ) + curLeft;
}
// 可以通过using属性定义钩子函数,取代默认的写入
if ( "using" in options ) {
options.using.call( elem, props );
} else {
// 默认直接通过{top: x, left: x}对象形式调用css写入
curElem.css( props );
}
}
};
jQuery.fn.extend( {
/* #10757 jQuery.fn.offset
* 相对页面(含滚动区)的坐标对象{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,
box = { top: 0, left: 0 },
elem = this[ 0 ],
doc = elem && elem.ownerDocument;
if ( !doc ) {
return;
}
docElem = doc.documentElement;
// 文档片段元素按照{left:0, top:0}返回
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)
if ( typeof elem.getBoundingClientRect !== "undefined" ) {
// 得到相对视口的坐标
box = elem.getBoundingClientRect();
}
win = getWindow( doc );
return {
// + 滚动距离 - 低版本IE的(2,2)修正
top: box.top + ( win.pageYOffset || docElem.scrollTop ) - ( docElem.clientTop || 0 ),
left: box.left + ( win.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 )
};
},
/* #10794 jQuery.fn.position
* 相对最近的有定位祖先的位置,margin范围算作元素的一部分
---------------------------------------------------------------------- */
position: function() {
if ( !this[ 0 ] ) {
return;
}
var offsetParent, offset,
parentOffset = { top: 0, left: 0 },
elem = this[ 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" ) {
// 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
// Subtract offsetParent scroll positions
// 修正,因为是到offsetParent的border内侧
//如果元素本身带滚动条,并且滚动了一段距离,那么两者间实际的偏移应该更多
parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true ) -
offsetParent.scrollTop();
parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true ) -
offsetParent.scrollLeft();
}
// 两offset()相减,margin需作为元素一部分,去掉
return {
top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ),
left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true )
};
},
/* #10837 jQuery.fn.offsetParent
* 获得元素集合对应的offsetParent(或html元素)集合
---------------------------------------------------------------------- */
offsetParent: function() {
return this.map( function() {
// 最近的有定位的上层元素
var offsetParent = this.offsetParent;
// display:none或position:fixed为null。正常元素若找不到则向上到body返回
while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) &&
jQuery.css( offsetParent, "position" ) === "static" ) ) {
offsetParent = offsetParent.offsetParent;
}
return offsetParent || documentElement;
} );
}
} );
/* #10851 jQuery.fn.scrollLeft/scrollTop
* 滚动距离
---------------------------------------------------------------------- */
jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) {
var top = /Y/.test( prop );
jQuery.fn[ method ] = function( val ) {
return access( this, function( elem, method, val ) {
// window有单独的推荐方法,所以先识别是否为window或document
var win = getWindow( elem );
if ( val === undefined ) {
// window,并且兼容pageXOffset,则使用推荐的window.pageXOffset
// 否则使用元素的scrollLeft/scrollTop方法,window则使用根元素的该方法
return win ? ( prop in win ) ? win[ prop ] :
win.document.documentElement[ method ] :
elem[ method ];
}
// window.scrollTo(xpos,ypos),对于不需改变的那个使用原值jQuery( win ).scrollxxx()
if ( win ) {
win.scrollTo(
!top ? val : jQuery( win ).scrollLeft(),
top ? val : jQuery( win ).scrollTop()
);
} else {
// elem.scrollLeft/scrollTop = xxx;
elem[ method ] = val;
}
}, method, val, arguments.length, null );
};
} );