[原创] jQuery源码分析-13 CSS操作-CSS-样式表-jQuery.fn.css()

作者:nuysoft/ 高云 QQ:47214707 Email:nuysoft@gmail.com
声明:本文为原创文章,如需转载,请注明来源并保留原文链接。

jQuery 源码分析系列(持续更新)

 

样式表

 

概述

如何使用

使用详解

    特性对应的实现原理

源码分析

    .css(name, value)

    jQuery.style(elem, name, value, extra)

    jQuery.css(elem, name, extra)

    curCSS(elem, name)

 

概述

 

CSS 操作部分的源码分析基于版本1.7.1,以后的 jQuery 源码分析系列将采用最新的版本。

jQuery.fn.css() 主要解决了三个问题:

浏览器兼容:IE、W3C

兼容 HTML 样式属性和 DOM 样式属性:连词符、驼峰

使设置元素的样式属性变得快速简单:动态参数检测、设置和读取用统一的接口

 

如何使用

 

jQuery.fn.css() 有4 种用法,第一种是读取样式属性值,其余三种是设置样式属性值

.css(propertyName) 获取第一个元素的样式属性值,propertyName 是 CSS 属性名

.css(propertyName, value) 在匹配的元素集上设置一个 CSS 属性,value 是要设置的属性值

.css(propertyName, function(index, value) ) 将函数返回值做为属性值设置

    function(index, value) 返回要设置的属性值,函数的上下文 this 指向当前元素,接收两个参数:index 是当前元素在集合中的下标位置;value 是旧值,即当前值(意味着设置之前要先取出当前值)

.css(map) 设置多个样式

    map 含有键值对的 map,键是属性名,值是字面直接量或函数

 

使用详解

 

读取和设置样式表遇到的难题是浏览器兼容性。例如访问样式属性时的方式就不同

// 在基于标准的浏览器中
var defaultView = elem && elem.ownerDocument.defaultView;
var computedStyle = defaultView && defaultView.getComputedStyle( elem, null );
var ret = computedStyle && computedStyle.getPropertyValue( name );
return ret;
// 相对的在 IE 中
var ret = elem.currentStyle && elem.currentStyle[ name ]
return ret;

另一个常见的兼容问题是,某些属性在不同的浏览器中使用不同的属性名,例如 float,在 IE 的 DOM 实现中用 styleFloat,而在遵守 W3C 标准的浏览器中是 cssFloat。

jQuery 的.css() 方法封装了这些差异,无论使用什么属性名都返回相同的结果。例如,一个向左浮动的元素,下边的三行代码每行都会返回字符串 left:

$('div.left').css('float');
$('div.left').css('cssFloat');
$('div.left').css('styleFloat');

如果遇到由多个单词组成的属性,这些属性在 CSS 和 DOM 有着不一样的格式,jQuery 也能等价的解释,例如:

.css({ 'background-color': '#ffe', 'border-left': '5px solid #ccc'} ).css( { backgroundColor: '#ffe', borderLeft: '5px solid #ccc'} )

jQuery 都能识别并返回正确的值,注意在DOM 属性的引号是可选的,而CSS 属性必须有引号,因为在属性名中有连字符 -

当使用.css() 设置样式时,jQuery 改变元素的样式属性 style property,例如下面两行代码是等价的:

$('#mydiv').css('color', 'green')
document.getElementById('mydiv').style.color = 'green'

为样式属性设置一个空字符串,例如 $('#mydiv').css('color', ''),如果这个属性是行内样式(HTML style attribute),这个属性会被从元素的 style 中移除,无论是通过.css() 方法操作,还是直接操作 DOM 样式属性 style(DOM style property);但是如果是定义在外部样式表 stylesheet 或内部样式表 <style> 元素中则不会移除(jQuery 的实现并不会修改外部样式表和内部样式表,这一点并不像有些书上写的,尽管浏览器提供了原生 API 支持)。

如果遇到CSS color,不同的浏览器可能返回逻辑上相等但是字面上不同的颜色值,总共有四种格式:#FFF、#ffffff、rgb(255,255,255)、blue。

但是.css()不支持 CSS 属性缩写,例如 margin background border。例如,如果想要获取外边距,需要使用 $(elem).css('marginTop') 和 $(elem).css('marginRight'),其他以此类推。

从 jQuery1.6 开始,.css() 可以支持相对值,就像.animate()。相对值是以 += 或 -= 开头的字符串,表示对当前值增加或减少。例如:一个元素的 padding-left 是 10px,.css("padding-left", "+=15") 使 padding-left 变为 25px。

从 jQuery1.4 开始,.css()允许传入一个函数作为属性值,例如在下面的这个例子中,将匹配元素的宽度设置为不断增大的值(递增):

$('elem.example').css('width', function(index) {
  return index * 50;
});

注意:如果函数没有返回任何值(例如 function(index, style){}),或返回 undefined,当前值不会改变。这一点很有用,如果需要只要当满足一定条件时,选择性的设置属性值时(函数不返回值或返回 undefined,与返回空字符串,有着截然不同的处理逻辑和结果)。

最后补充一点 CSS 的基础知识,参考 http://wenku.baidu.com/view/d9a18f7e27284b73f2425089.html:

1. CSS 样式表有三种写法:行内样式、文档内部样式、文档外部样式

2. 样式优先级:行内样式 > 内部样式 > 外部样式,ID 选择器 > class 选择器

 

将以上特性对应的实现原理简单阐述下(后边的源码分析会详细的解释):

设置和读取用都通过.css()

通过调用多功能工具函数 jQuery.access(详见03 构造 jQuery 对象 - 工具函数

访问样式属性时的方式不同

jQuery 加载执行时检测浏览器特性,将 getComputedStyle 或 currentStyle 统一为 jQuery 内部方法 curCSS()

某些属性在不同的浏览器中使用不同的属性名

jQuery.cssProps 中定义了属性名之间的映射关系

多个单词组成的样式属性在 CSS 和 DOM 有着不一样的格式

通过方法 jQuery.camelCase() 将连词符格式转为驼峰格式

相对值

通过 jQuery 内部正则 rrelNum = /^([\-+])=([\-+.\de]+)/ 检测并提取运算符和相对值,然后计算

函数的返回值作为属性值

通过调用多功能工具函数 jQuery.access 执行函数,并将返回值传给 jQuery.style

 

源码分析

 

.css(name, value)

jQuery.fn.css = function( name, value ) {
	// Setting 'undefined' is a no-op
	// 两个参数,value 为 undefined,则不做任何操作,返回 this
	// 即如果将一个样式属性设为 undefined,不做任何操作
	if ( arguments.length === 2 && value === undefined ) {
		return this;
	}
	// access: function(elems, key, value, exec, fn, pass) {
	// 调用多功能工具函数 jQuery.access,对 this 进行遍历,并执行参数中的函数
	return jQuery.access( this, name, value, true, function( elem, name, value ) {
		return value !== undefined ?
			jQuery.style( elem, name, value ) : // 设值,包括 value 是空字符串
			jQuery.css( elem, name ); // 取值
});

};

.css() 依赖于三个方法:

jQuery.access()这个全局方法支持.css()、.attr()、.prop(),分析详见03 构造 jQuery 对象 - 工具函数

jQuery.style() 在 DOM 节点上读取或设置样式属性(style property)

jQuery.css() 在 DOM 元素上读取 DOM 样式值

马上开始剖析 jQuery.style()和 jQuery.css()。

 

jQuery.style(elem, name, value, extra)

 

jQuery.style() 负责在 DOM 节点上读取或设置样式属性 style property(事实上在 CSS 模块中只用了 jQuery.style()的设置功能,读取功能和 jQuery.css() 有什么区别么?有待继续研究!)。

这个方法大致做了如下事

1. 过滤 Text 和 Comment,过滤无 style 的元素,返回 undefined

2. 转换为驼峰式,修正属性名

3. 如果是设置:

    如果是 number,过滤 NaN;过滤 null;如果是相对值字符串,计算

    添加后缀

    如果存在钩子,则调用钩子的 set;如果没有钩子,则设置 style[name] = value;

4. 如果是读取:

    如果存在钩子,则调用钩子的 get;如果没有钩子,则返回 style[name]

看看源码注释

// Get and set the style property on a DOM Node
// 在 DOM 节点上读取或设置样式属性 style property
style: function( elem, name, value, extra ) {
	// Don't set styles on text and comment nodes
	// 过滤 Text 和 Comment,如果没有 style 属性也返回
	if (!elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style) {
		return; // undefined
	}
<span style="color: rgba(0, 128, 0, 1)">// Make sure that we're working with the right name</span>
<span style="color: rgba(0, 128, 0, 1)">// 确保使用了正确的名字</span>
<span style="color: rgba(0, 0, 255, 1)">var</span> ret, type, origName = jQuery.camelCase( <span style="color: rgba(0, 0, 255, 1)">name</span> ),
	style = elem.style, hooks = jQuery.cssHooks[ origName ]; <span style="color: rgba(0, 128, 0, 1)">// 转换为驼峰格式</span>
<span style="color: rgba(0, 128, 0, 1)">// 修正属性名,是否在不同的浏览器中使用不同的属性名</span>
<span style="color: rgba(0, 0, 255, 1)">name</span> = jQuery.cssProps[ origName ] || origName; <span style="color: rgba(0, 128, 0, 1)">// CSS钩子</span>

<span style="color: rgba(0, 128, 0, 1)">// Check if we're setting a value</span>
<span style="color: rgba(0, 128, 0, 1)">// 设置</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> ( value !== undefined ) {
	type = <span style="color: rgba(0, 0, 255, 1)">typeof</span> value;

	<span style="color: rgba(0, 128, 0, 1)">// convert relative number strings (+= or -=) to relative numbers. #7345</span>
	<span style="color: rgba(0, 128, 0, 1)">// 计算相对值 rrelNum = /^([\-+])=([\-+.\de]+)/, //</span>
	<span style="color: rgba(0, 0, 255, 1)">if</span> ( type === "<span style="color: rgba(139, 0, 0, 1)">string</span>" &amp;&amp; (ret = rrelNum.exec( value )) ) {
		<span style="color: rgba(0, 128, 0, 1)">/*
	 	* ret[1] 正负;ret[2] 相对值
	 	* +( ret[1] + 1) ret[1]是字符串,加上1变成'+1'或'-1',最前边的加号将字符串转换为数字1或-1
		 * +ret[2] 同样的加号将ret[2]转换为数字
	 	* 正负1 乘以 相对值 再加上 当前值,得出要设置的值
		 */</span>
		value = ( +( ret[1] + 1) * +ret[2] ) + <span style="color: rgba(0, 0, 255, 1)">parseFloat</span>( jQuery.css( elem, <span style="color: rgba(0, 0, 255, 1)">name</span> ) );
		<span style="color: rgba(0, 128, 0, 1)">// Fixes bug #9237</span>
		<span style="color: rgba(0, 128, 0, 1)">// #9237:.css()在带有连字符的属性上不能工作,在1.6.2中修正</span>
		type = "<span style="color: rgba(139, 0, 0, 1)">number</span>";
	}

	<span style="color: rgba(0, 128, 0, 1)">// Make sure that NaN and null values aren't set. See: #7116</span>
	<span style="color: rgba(0, 128, 0, 1)">// 过滤NaN null,不做任何处理,如果想从内联样式中删除某个属性,请传入空字符串</span>
	<span style="color: rgba(0, 0, 255, 1)">if</span> ( value == <span style="color: rgba(0, 0, 255, 1)">null</span> || type === "<span style="color: rgba(139, 0, 0, 1)">number</span>" &amp;&amp; <span style="color: rgba(0, 0, 255, 1)">isNaN</span>( value ) ) {
		<span style="color: rgba(0, 0, 255, 1)">return</span>;
	}

	<span style="color: rgba(0, 128, 0, 1)">// If a number was passed in, add 'px' to the (except for certain CSS properties)</span>
	<span style="color: rgba(0, 128, 0, 1)">// 如果传入一个数字,追加单位px(jQuery.cssNumber中定义的属性除外,见jQuery.cssNumber的定义)</span>
	<span style="color: rgba(0, 0, 255, 1)">if</span> ( type === "<span style="color: rgba(139, 0, 0, 1)">number</span>" &amp;&amp; !jQuery.cssNumber[ origName ] ) {
		value += "<span style="color: rgba(139, 0, 0, 1)">px</span>";
	}

	<span style="color: rgba(0, 128, 0, 1)">// 前边的都是前戏:过滤非法参数、计算相对值、追加单位后缀</span>

	<span style="color: rgba(0, 128, 0, 1)">// If a hook was provided, use that value, otherwise just set the specified value</span>
	<span style="color: rgba(0, 128, 0, 1)">/*
	 * 如果有钩子hooks,且hooks中存在set函数,则调用hooks.set,将返回值赋给value
	 * 如果hooks.set的返回值为undefined,则不执行任何操作;返回值不为undefined,则用新value设置样式值
	 * 简单点说,有hooks.set则调用,用返回值替换value,最后设置style.name;否则直接设置style.name
	 * 可见钩子的作用是修正属性值,并不直接对值进行设置
	 * 等价的逻辑:
	 * &lt;pre&gt;
	 * if ( hooks &amp;&amp; "set" in hooks ) {
	 *   value = hooks.set( elem, value );
	 *   if( value != undefined ) style[ name ] = value;
	 * } else {  
	 *   style[ name ] = value;
	 * }
	 * &lt;/pre&gt;
	 */</span>
	<span style="color: rgba(0, 0, 255, 1)">if</span> ( !hooks || !("<span style="color: rgba(139, 0, 0, 1)">set</span>" <span style="color: rgba(0, 0, 255, 1)">in</span> hooks) || (value = hooks.set( elem, value )) !== undefined ) {
		<span style="color: rgba(0, 128, 0, 1)">// Wrapped to prevent IE from throwing errors when 'invalid' values are provided</span>
		<span style="color: rgba(0, 128, 0, 1)">// Fixes bug #5509</span>
		<span style="color: rgba(0, 128, 0, 1)">// 用try-catch块,预防在IE中,当用不合法的值设置样式值时,抛出异常</span>
		<span style="color: rgba(0, 0, 255, 1)">try</span> {
			style[ <span style="color: rgba(0, 0, 255, 1)">name</span> ] = value;
		} <span style="color: rgba(0, 0, 255, 1)">catch</span>(e) {}
	}
<span style="color: rgba(0, 128, 0, 1)">// 读取</span>
} <span style="color: rgba(0, 0, 255, 1)">else</span> {
	<span style="color: rgba(0, 128, 0, 1)">// If a hook was provided get the non-computed value from there</span>
	<span style="color: rgba(0, 128, 0, 1)">// 如果有钩子hooks,则调用hooks.get,返回值赋给ret</span>
	<span style="color: rgba(0, 0, 255, 1)">if</span> ( hooks &amp;&amp; "<span style="color: rgba(139, 0, 0, 1)">get</span>" <span style="color: rgba(0, 0, 255, 1)">in</span> hooks &amp;&amp; (ret = hooks.get( elem, <span style="color: rgba(0, 0, 255, 1)">false</span>, extra )) !== undefined ) {
		<span style="color: rgba(0, 0, 255, 1)">return</span> ret;
	}

	<span style="color: rgba(0, 128, 0, 1)">// Otherwise just get the value from the style object</span>
	<span style="color: rgba(0, 128, 0, 1)">// 否则从style对象中读取属性值</span>
	<span style="color: rgba(0, 0, 255, 1)">return</span> style[ <span style="color: rgba(0, 0, 255, 1)">name</span> ];
}

}

 

jQuery.css(elem, name, extra)

 

jQuery.css() 负责读取样式值。

这个方法大致做了如下事

1. 转换为驼峰式,修正属性名

2. 如果有钩子,则调用钩子的 get

3. 否则调用 curCSS,不同的浏览器调用不同的方法:

    IE:getComputedStyle,elem.ownerDocument.defaultView.getComputedStyle(elem, null).getPropertyValue(name)

    W3C:currentStyle,elem.currentStyle[name]

看看源码注释

// 读取样式值
css: function( elem, name, extra ) {
	var ret, hooks;

	// Make sure that we're working with the right name
	name = jQuery.camelCase( name ); // 转换为驼峰式
	hooks = jQuery.cssHooks[ name ]; // 是否有钩子
	name = jQuery.cssProps[ name ] || name; // 修正属性名

	// cssFloat needs a special treatment
	// cssFloat 需要特殊处理,(styleFloat 不需要吗?)
	if ( name === "cssFloat" ) {
		name = "float"; // 又把它转换回去了!
	}

	// If a hook was provided get the computed value from there
	// 如果钩子 hooks 存在,则调用 hooks.get 计算样式值,并返回
	if ( hooks && "get" in hooks && (ret = hooks.get( elem, true, extra ))!== undefined) {
		return ret;

	// Otherwise, if a way to get the computed value exists, use that
	// 否则,如果 curCSS 存在,则调用 curCSS 获取计算后的样式值,并返回
	} else if (curCSS) {
		return curCSS( elem, name );}
}

 

curCSS(elem, name)

 

/**
 * 标准
 */
if ( document.defaultView && document.defaultView.getComputedStyle ) {
	getComputedStyle = function( elem, name ) {
		var ret, defaultView, computedStyle; // 预定义变量
		// 将驼峰式转换为连字符,例如 marginTop > margin-top
		// rupper = /([A-Z]|^ms)/g,
		name = name.replace( rupper, "-$1" ).toLowerCase();
	<span style="color: rgba(0, 128, 0, 1)">/*
	 * 分解:
	 * var defaultView = elem &amp;&amp; elem.ownerDocument.defaultView;
	 * var computedStyle = defaultView &amp;&amp; defaultView.getComputedStyle( elem, null );
	 * var ret = computedStyle &amp;&amp; computedStyle.getPropertyValue( name );
	 * return ret;
	 */</span>
	<span style="color: rgba(0, 0, 255, 1)">if</span> ( (defaultView = elem.ownerDocument.defaultView) &amp;&amp;
			(computedStyle = defaultView.getComputedStyle( elem, <span style="color: rgba(0, 0, 255, 1)">null</span> )) ) {
		ret = computedStyle.getPropertyValue( <span style="color: rgba(0, 0, 255, 1)">name</span> );
		<span style="color: rgba(0, 128, 0, 1)">// 看不懂这行在干什么?</span>
		<span style="color: rgba(0, 0, 255, 1)">if</span> ( ret === "<span style="color: rgba(139, 0, 0, 1)"></span>" &amp;&amp; !jQuery.contains( elem.ownerDocument.documentElement, elem ) ) {
			ret = jQuery.style( elem, <span style="color: rgba(0, 0, 255, 1)">name</span> );
		}
	}

	<span style="color: rgba(0, 0, 255, 1)">return</span> ret;
};

}
/**

  • IE
    */
    if ( document.documentElement.currentStyle ) {
    currentStyle = function( elem, name ) {
    var left, rsLeft, uncomputed,
    ret = elem.currentStyle && elem.currentStyle[ name ], // 直接就取值
    style = elem.style;

     <span style="color: rgba(0, 128, 0, 1)">// Avoid setting ret to empty string here</span>
     <span style="color: rgba(0, 128, 0, 1)">// so we don't default to auto</span>
     <span style="color: rgba(0, 128, 0, 1)">/*
      * 避免返回空字符串,看不懂?
      * 如果elem.currentStyle[ name ]返回null,用style[name]试试
      */</span>
     <span style="color: rgba(0, 0, 255, 1)">if</span> ( ret === <span style="color: rgba(0, 0, 255, 1)">null</span> &amp;&amp; style &amp;&amp; (uncomputed = style[ <span style="color: rgba(0, 0, 255, 1)">name</span> ]) ) {
     	ret = uncomputed;
     }
    
     <span style="color: rgba(0, 128, 0, 1)">// From the awesome hack by Dean Edwards</span>
     <span style="color: rgba(0, 128, 0, 1)">// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291</span>
    
     <span style="color: rgba(0, 128, 0, 1)">// If we're not dealing with a regular pixel number</span>
     <span style="color: rgba(0, 128, 0, 1)">// but a number that has a weird ending, we need to convert it to pixels</span>
     <span style="color: rgba(0, 128, 0, 1)">/*
      * 不处理一般的像素值,但是如果单位很奇怪就需要修正为像素px
      * rnumpx = /^-?\d+(?:px)?$/i, // 可选的负号 加 数字 加 可选的px,对数值进行检查
      * rnum = /^-?\d/, // 整数,不支持+1这样的写法(应该支持)
      * 
      * 数字后跟了非像素单位
      * 
      * 后边的看不懂啊,应该是修正单位、auto、fontSize
      */</span>
     <span style="color: rgba(0, 0, 255, 1)">if</span> ( !rnumpx.test( ret ) &amp;&amp; rnum.test( ret ) ) {
    
     	<span style="color: rgba(0, 128, 0, 1)">// Remember the original values</span>
     	<span style="color: rgba(0, 128, 0, 1)">// 记录原始值</span>
     	left = style.left;
     	rsLeft = elem.runtimeStyle &amp;&amp; elem.runtimeStyle.left; <span style="color: rgba(0, 128, 0, 1)">// </span>
    
     	<span style="color: rgba(0, 128, 0, 1)">// Put in the new values to get a computed value out</span>
     	<span style="color: rgba(0, 0, 255, 1)">if</span> ( rsLeft ) { 
     		elem.runtimeStyle.left = elem.currentStyle.left;
     	}
     	style.left = <span style="color: rgba(0, 0, 255, 1)">name</span> === "<span style="color: rgba(139, 0, 0, 1)">fontSize</span>" ? "<span style="color: rgba(139, 0, 0, 1)">1em</span>" : ( ret || 0 );
     	ret = style.pixelLeft + "<span style="color: rgba(139, 0, 0, 1)">px</span>";
    
     	<span style="color: rgba(0, 128, 0, 1)">// Revert the changed values</span>
     	style.left = left;
     	<span style="color: rgba(0, 0, 255, 1)">if</span> ( rsLeft ) {
     		elem.runtimeStyle.left = rsLeft;
     	}
     }
    
     <span style="color: rgba(0, 0, 255, 1)">return</span> ret === "<span style="color: rgba(139, 0, 0, 1)"></span>" ? "<span style="color: rgba(139, 0, 0, 1)">auto</span>" : ret;
    

    };
    }

curCSS = getComputedStyle || currentStyle;

 

后记:本人对 CSS 能熟练使用但不精通,看源码的过程遇到很多疑问,不懂的地方文中有标记,各位同学如果能解疑或有好的参考资料多多拍砖。本人、本文、本系列均不是权威,请持怀疑态度。