jQuery-1.9.1源码分析系列(九) CSS操作
jquery.fn.css获取当前jQuery所匹配的元素中第一个元素的属性值【$(…).css(cssName),注意这个cssName可以是数组】或给当前jQuery所匹配的每个元素设置样式值【$(…).css(cssname,value) / $(…).css(obj)】;
可以看见函数内部直接调用了jquery.access来处理。access将当前多个元素组成的jQuery对象所匹配的元素分解成单一元素逐个调用第二个参数中的回调function( elem, name, value );如果参数name是对象的话,access内部分解name递归调用逐个处理name的每一个key/value键值对
源码
jQuery.fn.css: function( name, value ) { //access将当前jQuery对象分解成单一元素逐个调用第二个参数中的回调function( elem, name, value ), //如果参数name是对象的话,access内部分解name递归调用逐个处理name的每一个key/value键值对 return jQuery.access( this, function( elem, name, value ) { var len, styles, map = {}, i = 0; //如果css特征名称是一个数组,比如['left','marginRight'] if ( jQuery.isArray( name ) ) { styles = getStyles( elem ); len = name.length; //通过$.css()获取对应的css特征值 for ( ; i < len; i++ ) { map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); } return map; } //value有值则调用$.style设置单个css值,value参数无值则通过$.css()获取对应的css特征值 return value !== undefined ? jQuery.style( elem, name, value ) : jQuery.css( elem, name ); }, name, value, arguments.length > 1 ); }
这个api比较简单,但是仔细分析里面所调用的函数会发现一大堆的知识。后面一一来发掘。
首先我们看到getStyles(elem)这个函数,看一看他的定义
if ( window.getComputedStyle ) { getStyles = function( elem ) { return window.getComputedStyle( elem, null ); }; ... //ie8-兼容 } else if ( document.documentElement.currentStyle ) { getStyles = function( elem ) { return elem.currentStyle; }; ... }
两个不同的方法window.getComputedStyle和elem.currentStyle。接下来一一分析他们。
a. window.getComputedStyle
完整的表达式window.getComputedStyle(elem,pseudo)
elem: DOM节点,必须
pseudo: 伪类,且只能是伪元素的伪类,比如::after,::before,::first-letter,::first-line。可选【Gecko 2.0 (Firefox 4 / Thunderbird 3.3 / SeaMonkey 2.1) 之前,第二个参数“伪类”是必需的】
说明:这个函数取出来的是CSS计算后的最终使用的CSS属性值【其中不包括位置关系left/right/top/bottom的属性值,比如left:10%,那么通过getComputedStyle获取的left还是"10%"】(Safari 5.1.7的margin-right返回的是百分比,这个需要特殊处理)。只读。在现代浏览器中支持良好,不支持IE8-
举例说明(这个例子会贯穿全文):
<style>
#myList{
height: 100px;
}
#myList:after{
content: 'a';
height: 100px;
}
</style>
<ul id="myList" style='width:50%;left:666px;'><li>test</li></ul>
<ul id="myList2" style='width:50%;left:111px;'><li>test2</li></ul>
getComputedStyle的值是计算后的值(比如百分比会转换成像素),实例如下
宽度50%被转换成像素了。
getComputedStyle中第二个参数只有是伪元素的伪类才起作用,实例如下
如上图可知,只要后面的伪元素不对或没有传递伪元素伪类这个参数,则得到的还是第一个参数对应的元素的getComputedStyle值。上图中.left和.content属性可作证。
getComputedStyle得到的是一个只读数组对象,数组的每个元素是一个CSS样式名称,并且这个只读数组对象拥有与数组中每个元素一一对应的属性来保存CSS样式的值。我们先看一下结构(window.getComputedStyle(document.getElementById('myList'),null))
但是由于不支持IE8-那么我们需要一个IE8能支持的方法。这就是IE自己的东东elem.currentStyle
b. elem.currentStyle
elem.currentStyle也是一个只读的对象。
elem.currentStyle与getComputedStyle的不同
1. elem.currentStyle在IE8-中是一个纯对象,没有类数组的结构,不可以通过style[n]方式获得CSS样式名称;在IE9+中elem.currentStyle才是一个类数组对象。
2. elem.currentStyle获得的样式虽然是最终的CSS样式,但是并非计算过的样式,比如同样是先前的例子
getComputedStyle的width结果是像素值632px
而elem.currentStyle的结果是设置的百分比值50%
3. 样式名称的差异,比如对于float属性,IE8-中currentStyle是styleFloat
而firefox中getComputedStyle是cssFloat和float【建议不要使用,浏览器保留,而且也非标准方法,浏览器可以不支持】并存
chrome中getComputedStyle是显示是float,实际上通过float【建议不要使用,浏览器保留,而且也非标准方法,浏览器可以不支持】和cssFloat都可以获取到
而IE9是cssFloat和styleFloat都有。
所以jQuery获取float属性的方式是"float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat"
c. elem.style
elem.style和window.getComputedStyle(elem,pseudo)以及elem.currentStyle比较
elem.style是获取elem的内联样式。这是和window.getComputedStyle(elem,pseudo)以及elem.currentStyle相比不同的地方之一。
style结果可读可写,而getComputedStyle和currentStyle的结果只读。
style结果是没有计算的结果,这一点和currentStyle类似,getComputedStyle是计算过的结果。如下图中style.width的值是50%,而不是像素值
d. 现代浏览器获取样式表中某个样式的值--getPropertyValue
getPropertyValue(className)是现代浏览器(IE9+,firefox,chrome...)样式表的一个属性方法。所以只要是现代浏览器,上面getComputedStyle/currentStyle/style三种方式获取的样式表style都可以使用该方法获取样式值。例如style.getPropertyValue("float")。
需要注意的是className是直接属性名称(比如"background-color")。是“float”而非“css-float”或“cssFloat”。
如果是使用属性获取的方式获取float值,则需要转换成"cssFloat"或"styleFloat",比如
这个比较折腾。
而由于IE8-又不支持该方法,IE8-直接使用属性获取方式
IE8-的currentStyle还支持另一种获取属性方法:style.getAttribute(className)。
需要注意的是className是可以是直接属性名称或是驼峰写法(比如"background-color"或“backgroundColor"都可以)。
所以,我们为了兼容的话,有两种处理样式的方法
1. 结合getPropertyValue(className)和getAttribute(className)使用,因为他们两className都可以是直接属性名称
2. 使用属性获取方式style[className]但是需要注意的是属性名称需要兼容,比如:"float"需要替换成"cssFloat"或"styleFloat"。
而jQuery的处理就是选择第二种方式。
说道这里,虽然jQuery使用第二种方式,但是有一个属性使用属性方式获取会失败,这就是奇葩的"filter"属性。这个属性必须使用getPropertyValue才能获取正确
用表格总结一下CSS属性获取的相关用法
特点 | getComputedStyle | currentStyle | style |
浏览器支持情况 | IE9+,chrome,firefox... | IE | ALL |
可读可写 | 只读 | 只读 | 可读可写 |
是否为计算最终值(比如百分比、比例都计算为真实的像素值) | 是 | 否 | 否 |
(外部样式表+内部样式表+内联样式)的最终结果 | 是 | 是 | 否(只是内联样式) |
属性获取方式style.name和style[name] | 支持("filter"属性除外) | 支持 | 支持 |
支持的获取CSS属性值的方法 | getPropertyValue | IE8-只支持getAttribute;IE9+支持getPropertyValue和getAttribute("filter") | IE8-只支持getAttribute;IE9+支持getPropertyValue和getAttribute("filter");其他浏览器只支持getPropertyValue |
突出优点 | 标准化,支持伪元素(如::after),获取的是计算后的结果 | 可读可写 | |
这一章字数已经不少了,我先前还以为这一章会比较简单的,没有想到拓展开来这么多!骚年们,耐心看下去。。。
接下来继续分析源码:
既然了解了各种获取CSS样式表的方法,接下来看一个获取指定的CSS样式名称对应的计算值得函数curCSS = function( elem, name, _computed ) 。难点在于通过currentStyle和getComputedStyle获取到的值可能是百分比或相对值得时候,我们需要进行模拟计算出真实的值。过程比较点单,看源码注释
//备注:我们在window.getComputedStyle包含了"window" //因为node.js中的js DOM如果没有他的话将被终止 if ( window.getComputedStyle ) { getStyles = function( elem ) { return window.getComputedStyle( elem, null ); }; curCSS = function( elem, name, _computed ) { var width, minWidth, maxWidth, computed = _computed || getStyles( elem ), // getPropertyValue只有在IE9的 .css('filter')中用到 ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined, style = elem.style; if ( computed ) { if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { ret = jQuery.style( elem, name ); } // Chrome < 17 and Safari 5.0使用"计算结果"替代"使用的值"来计算margin-right // Safari 5.1.7 (最新)返回百分比但是我们需要像素值,这一点违背CSSOM草案 //http://dev.w3.org/csswg/cssom/#resolved-values
//模拟计算 if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { //保存原值 width = style.width; minWidth = style.minWidth; maxWidth = style.maxWidth; //放入新的值来获得计算值,比如marginRight为10%的时候,通过放入width为10%,然后通过computed.width即可获得10%对应的px宽度 style.minWidth = style.maxWidth = style.width = ret; ret = computed.width; //还原更改的值 style.width = width; style.minWidth = minWidth; style.maxWidth = maxWidth; } } return ret; }; //低版本ie兼容 } else if ( document.documentElement.currentStyle ) { getStyles = function( elem ) { return elem.currentStyle; }; curCSS = function( elem, name, _computed ) { var left, rs, rsLeft, computed = _computed || getStyles( elem ), ret = computed ? computed[ name ] : undefined, style = elem.style; //这里避免给ret设置空字符 //所以我们不默认为”auto” if ( ret == null && style && style[ name ] ) { ret = style[ name ]; } // 我们对以奇怪结尾的数字(比如1em)要把他转化成像素 // 但是不能是位置属性,应为他们是和父元素成比例的,并且我们不能测量父元素的比例,因为他可能是一大堆比例堆叠而成(比如<div style=’left:10%’><p style=’left:20%’><span style=’left:20%’></span></p></div>),根本无法计算 if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { //保存原值 left = style.left; rs = elem.runtimeStyle; rsLeft = rs && rs.left; //放入新的值来获得计算值 if ( rsLeft ) { rs.left = elem.currentStyle.left; } style.left = name === "fontSize" ? "1em" : ret; ret = style.pixelLeft + "px"; //还原更改的值 style.left = left; if ( rsLeft ) { rs.left = rsLeft; } } return ret === "" ? "auto" : ret; }; }
根据jQuery.fn.css函数最后的处理
return value !== undefined ? jQuery.style( elem, name, value ) : jQuery.css( elem, name );
可知css处理的两个关键低级api:jQuery.style和jQuery.css。
前面已经分析了只有.style是可读可写的,同理,这里的jQuery.style作用也是用来读写内联样式的。jQuery.style的处理流程为
1.修正css特征名称保存为origName,真正能被浏览器识别的名称保存为name。
2.查找name或origName的cssHooks。
3.如果是设置值(有传递value参数),则设置之。其中比较特殊的处理是value可以是累计字符串(如:“+=”)需要先通过jQuery.css取出原来的值来计算。如果value是数字,需要根据情况看是否添加“px”单位。如果是有相应的cssHooks也要特殊处理等等。
4.如果是获取值(没有传递value参数),分两种情况,有hooks通过hooks获取,否则直接style[name]即可。
源码
//给DOM节点设置或获取样式特征值 jQuery.style: function( elem, name, value, extra ) { //文本和注释节点不能设置样式特征值 if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { return; } //修正css特征名称以适配当前浏览器 var ret, type, hooks, origName = jQuery.camelCase( name ), style = elem.style; //jQuery.cssProps缓存查询过的css特征名称供后续便捷查找使用 name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); //获取有前缀版本的hooks或没有前缀版本的hooks hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; //如果是给css特征名称设置值 if ( value !== undefined ) { type = typeof value; // convert relative number strings (+= or -=) to relative numbers. #7345 //rrelNum = new RegExp( "^([+-])=(" + core_pnum + ")", "i" ) //转换相对数字符串+=/-=为相应的数字 if ( type === "string" && (ret = rrelNum.exec( value )) ) { //(+ + 1) == 1;(- + 1) == -1 value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); // Fixes bug #9237 type = "number"; } //NaN和null不可用 if ( value == null || type === "number" && isNaN( value ) ) { return; } //除开不可设置为像素单位的css特征,其他的都添加上“px” if ( type === "number" && !jQuery.cssNumber[ origName ] ) { value += "px"; } //support.clearCloneStyle = div.style.backgroundClip === "content-box"; //div.style.backgroundClip为非“content-box”模式且 //设置的值为空的background...将其设置为继承父节点样式 // Fixes #8908, 更准确的做法是对每一个问题特征都设置默认值,但是这至少会调用8次函数 if ( !jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) { style[ name ] = "inherit"; } //如果提供了hook则使用hook设置值,否则设置置顶的值 if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { //当要设置的值为无效值的时候ie会抛出异常 // Fixes bug #5509 try { style[ name ] = value; } catch(e) {} } //获取值 } else { //如果提供了hook则使用hook取值 if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { return ret; } // 其他情况从style对象中取值 return style[ name ]; } } //返回一个css特征名称,该名称可能是供应商添加了前缀的特征名 function vendorPropName( style, name ) { //短名称未添加厂商前缀 if ( name in style ) {return name; } //检查供应商的前缀名 //cssPrefixes = [ "Webkit", "O", "Moz", "ms" ] var capName = name.charAt(0).toUpperCase() + name.slice(1), origName = name, i = cssPrefixes.length; while ( i-- ) { name = cssPrefixes[ i ] + capName; if ( name in style ) {return name; } } return origName; }
jQuery.css处理也比较简单
1.修正css特征名称保存为origName,真正能被浏览器识别的名称保存为name
2.如果存在相应的cssHooks,则处理之;否则使用curCSS方法获取样式值
3.对获取到的样式值做一些默认值得处理,比如css样式fontWeight的默认值为“normal”对应的值应该是400。
源码
//获取name对应的css特征值 css: function( elem, name, extra, styles ) { var num, val, hooks, origName = jQuery.camelCase( name ); //修正名称 name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); //获取有前缀版本的hooks或没有前缀版本的hooks hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; //从hook中提取值 if ( hooks && "get" in hooks ) { val = hooks.get( elem, true, extra ); } //其他情况使用curCSS取值 if ( val === undefined ) { val = curCSS( elem, name, styles ); } //cssNormalTransform = {letterSpacing: 0,fontWeight: 400} //将"normal"转化成计算值 if ( val === "normal" && name in cssNormalTransform ) { val = cssNormalTransform[ name ]; } //当强制或提供了限定和val看上去像数字时强制转化成数字并返回 if ( extra === "" || extra ) { num = parseFloat( val ); return extra === true || jQuery.isNumeric( num ) ? num || 0 : val; } return val; }
OK,这一章终于搞定了。