闲话
原计划是没有这篇博文的,研究animation源码的时候遇到了css样式这个拦路虎。比如jQuery支持“+=10”、“+=10px”定义一个属性的增量,但是有的属性设置时可以支持数字,有的必须有单位;在对属性当前值读取时,不同的浏览器可能返回不同的单位值,无法简单的相加处理;在能否读取高宽等位置信息上,还会受到display状态的影响;不同浏览器,相同功能对应的属性名不同,可能带有私有前缀等等。
众多的疑问,让我决定先破拆掉jQuery的样式机制。
(本文采用 1.12.0 版本进行讲解,用 #number 来标注行号)
css
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的思路是抽象一个标准化的流程,然后对每一个可能存在例外的地方安放钩子,对于需要例外的情形,只需外部定义对应的钩子即可调整执行过程,即标准化流程 + 钩子。
下面我们来逐个击破!
1、access
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。
2、jQuery.fn.css
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 );
};
3、属性名兼容
第一步: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;
}
}
}
4、jQuery.css、jQuery.style
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