jQuery源码解析之CSS样式方法讲解
2017-03-12 22:29

jQuery的css 方法源码解析

jquery的css方法的用来获取或设置元素的css样式,我们来看jquery中是如何实现这么一个强大的方法的。

jQuery.fn.extend({
	css: function( name, value ) {
		return jQuery.access( this, function( elem, name, value ) {
			return value !== undefined ?
				jQuery.style( elem, name, value ) :
				jQuery.css( elem, name );
		}, name, value, arguments.length > 1 );
	},
}

css方法同样是使用jquery的access()方法包装的。所以,css方法的调用也可以使用access方法提供的四种调用方式。

上面这段代码很好理解,如果只有样式名name,获取样式值调用jQuery.css;如果样式名,样式值都有,设置样式调用jQuery.style方法。

jQuery.style方法

我们先看jQuery.style方法的源码

// 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
	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 NaN and null values aren't set. See: #7116
		if ( value == null || type === "number" && isNaN( value ) ) {
			return;
		}

		// If a number was passed in, add 'px' to the (except for certain CSS properties)
		if ( type === "number" && !jQuery.cssNumber[ origName ] ) {
			value += "px";
		}

		// 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 ) {
			// Wrapped to prevent IE from throwing errors when 'invalid' values are provided
			// Fixes bug #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 ];
	}
},

代码不少,别怕,逐步分析,一点点消化。

// Don't set styles on text and comment nodes
if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
	return;
}

如果元素为空,或者是文本节点,注释节点,或者元素没有style属性,则return返回。什么元素没有style呢?document,window对象都没有style属性的。

// 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 ) );
// Convert dashed to camelCase; used by the css and data modules
// Microsoft forgot to hump their vendor prefix (#9572)
camelCase: function( string ) {
	return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
},
rmsPrefix = /^-ms-/,
rdashAlpha = /-([\da-z])/gi,
fcamelCase = function( all, letter ) {
	return ( letter + "" ).toUpperCase();
},

a.style.cssText = "top:1px;float:left;opacity:.5";
// Verify style float existence
// (IE uses styleFloat instead of cssFloat)
cssFloat: !!a.style.cssFloat,
cssProps: {
	// normalize float css property
	"float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat"
},
function vendorPropName( style, name ) {

	// shortcut for names that are not vendor prefixed
	if ( name in style ) {
		return name;
	}

	// check for vendor prefixed names
	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;
}
cssPrefixes = [ "Webkit", "O", "Moz", "ms" ],

处理样式名的格式化代码,以及用到的相关正则表达式和方法如上。

camelCase方法用来将“-ms-transition”转换成“msTransition”,这个方法先通过replace将“-ms-”替换成“ms-”,然后通过replace第二个参数为函数的方式将"ms-transition"替换成“msTransition”。因为其他前缀“WebkitTransition”第一个字母是大写,而"ms"是首字母是小写,所以要多“-ms-”先转换成“ms-”。这个方法同样也会将“background-color”转变成“backgroundColor”。这个是对带前缀的样式名转换成驼峰式写法。

然后name样式名通过cssProps将“float”转换成IE中的“cssFloat”或者其他浏览器下“styleFloat”。这是float在各个浏览器下的兼容处理。如果name样式名不是“float”,那么就调用"vendorPropName"来获取格式化的样式名。

“vendorPropName”方法中,判断“name in style”则是没有前缀的样式名,直接返回。否则将样式的第一个字母大写,循环在前面加上"cssPrefixes"数组中的前缀,如果“name in style”为true,则返回加上前缀的样式名。这样就可以通过传入“transiton”样式名,来匹配浏览器看是用“WebkitTransiton”还是“MozTransition”。这是对写w3c的规范样式名添加对应的前缀。

所以使用jQuery的css方法,使用规范的样式名,不带前缀,兼容性更好。

而且jquery使用“jQuery.cssProps[origName]=vendorPropName(style,origName)”,这样每次获取对应的前缀名后就会在cssProps对象上添加一个对应的样式名属性,下次再使用此样式名,就不用再调用vendorPropName方法。算是一种优化方式。

// gets hook for the prefixed version
// followed by the unprefixed version
hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
// Add in style property hooks for overriding the default
// behavior of getting and setting a style property
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;

		}
	    }
    }
},
jQuery.each([ "height", "width" ], function( i, name ) {
	jQuery.cssHooks[ name ] = {
		get: function( elem, computed, extra ) {
			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
				if ( elem.offsetWidth === 0 && rdisplayswap.test(curCSS( elem, "display" ))) {
					return jQuery.swap( elem, cssShow, function() {
						return getWidthOrHeight( elem, name, extra );
					});
				} else {
					return getWidthOrHeight( elem, name, extra );
				}
			}
		},

		set: function( elem, value, extra ) {
			return setPositiveNumber( elem, value, extra ?
				augmentWidthOrHeight(
					elem,
					name,
					extra,
					jQuery.support.boxSizing && jQuery.css( elem, "boxSizing" )==="border-box"
				) : 0
			);
		}
	};
});
a.style.cssText = "top:1px;float:left;opacity:.5";
support.opacity= /^0.5/.test( a.style.opacity );
if ( !jQuery.support.opacity ) {
	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 >= 1 && 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 there is no filter style applied in a css rule, we are done
				if ( currentStyle && !currentStyle.filter ) {
					return;
				}
			}

			// otherwise, set new filter values
			style.filter = ralpha.test( filter ) ?
				filter.replace( ralpha, opacity ) :
				filter + " " + opacity;
		}
	};
}
// These hooks cannot be added until DOM ready because the support test
// for it is not run until after DOM ready
jQuery(function() {
	if ( !jQuery.support.reliableMarginRight ) {
		jQuery.cssHooks.marginRight = {
			get: function( elem, 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" }, function() {
					if ( computed ) {
						return curCSS( elem, "marginRight" );
					}
				});
			}
		};
	}

	// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
	// getComputedStyle returns percent when specified for top/left/bottom/right
	// rather than make the css module depend on the offset module, we just check for it here
	if ( !jQuery.support.pixelPosition && jQuery.fn.position ) {
		jQuery.each( [ "top", "left" ], function( i, prop ) {
			jQuery.cssHooks[ prop ] = {
				get: function( elem, computed ) {
					if ( computed ) {
						var ret = curCSS( elem, prop );
						// if curCSS returns percentage, fallback to offset
						return rnumnonpx.test( ret ) ? 
						    jQuery( elem ).position()[ prop ] + "px" : ret;
					}
				}
			};
		});
	}

});
// These hooks are used by animate to expand properties
jQuery.each({
	margin: "",
	padding: "",
	border: "Width"
}, function( prefix, suffix ) {
	jQuery.cssHooks[ prefix + suffix ] = {
		expand: function( value ) {
			var i,

			// assumes a single number if not a string
			parts = typeof value === "string" ? value.split(" ") : [ value ],
			expanded = {};

			for ( i = 0; 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;
	}
});

attr方法中的hook方法一样,css方法也是通过hook方法来架构一些css样式的兼容处理扩展。有opacity,margin,width,height等样式的兼容。具体的兼容处理后文中再分篇讲解,本文主要讲解css方法的源码。

// 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 NaN and null values aren't set. See: #7116
	if ( value == null || type === "number" && isNaN( value ) ) {
		return;
	}

	// If a number was passed in, add 'px' to the (except for certain CSS properties)
	if ( type === "number" && !jQuery.cssNumber[ origName ] ) {
		value += "px";
	}

	// 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 ) {
		// Wrapped to prevent IE from throwing errors when 'invalid' values are provided
		// Fixes bug #5509
		try {
			style[ name ] = value;
		} catch(e) {}
	}

}

value不是undefined的时候,设置css样式的属性。

“typeof value”用来判断value的数据类型。

// Used for matching numbers
core_pnum = /[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source,
rrelNum = new RegExp( "^([-+])=(" + core_pnum + ")", "i" ),

这个正则用来匹配"+=10","-=15"等写法,匹配结果ret数组第一个元素表示加减号,第二个参数表示数值,和相关样式原始值进行相加,获取样式值结果。

// Make sure that NaN and null values aren't set. See: #7116
if ( value == null || type === "number" && isNaN( value ) ) {
	return;
}

当value为null,或者value的值不是数值,“type='number'&&isNaN(value)”是接上一个if判断,+=,-=写法后,type定义为了“number”,如果value不是不是数值,就return 返回。

// If a number was passed in, add 'px' to the (except for certain CSS properties)
if ( type === "number" && !jQuery.cssNumber[ origName ] ) {
	value += "px";
}
// Exclude the following css properties to add px
cssNumber: {
	"fillOpacity": true,
	"fontWeight": true,
	"lineHeight": true,
	"opacity": true,
	"orphans": true,
	"widows": true,
	"zIndex": true,
	"zoom": true
},

这句是判断样式值是否是"px"单位的样式,如果是则加上“px”。

// 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 ) {
	// Wrapped to prevent IE from throwing errors when 'invalid' values are provided
	// Fixes bug #5509
	try {
		style[ name ] = value;
	    } catch(e) {}
}

如果hook兼容处理可以处理,那么使用hook的set方法处理,不然就用"style[name]=value"来设置样式值。这里是对设置css样式的hook兼容的调用。

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 ];
}

else中是对value为undefined时,获取样式值的hook调用,或者返回“style[name]”获取的样式值。

jQuery.style方法的源码我们看完了,这个方法主要用过通过hooks的兼容来设置css样式,下面我们接着看jQuey.css方法:

jQuery.css方法

css: function( elem, name, numeric, extra ) {
	var val, num, hooks,
	    origName = jQuery.camelCase( name );

	// Make sure that we're working with the right name
	name = jQuery.cssProps[ origName ] 
	        || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) );

	// gets hook for the prefixed version
	// followed by the unprefixed version
	hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];

	// If a hook was provided get the computed value from there
	if ( hooks && "get" in hooks ) {
		val = hooks.get( elem, true, extra );
	}

	// Otherwise, if a way to get the computed value exists, use that
	if ( val === undefined ) {
		val = curCSS( elem, name );
	}

	//convert "normal" to computed value
	if ( val === "normal" && name in cssNormalTransform ) {
		val = cssNormalTransform[ name ];
	}

	// Return, converting to number if forced or a qualifier was provided and val looks numeric
	if ( numeric || extra !== undefined ) {
		num = parseFloat( val );
		return numeric || jQuery.isNumeric( num ) ? num || 0 : val;
	}
	return val;
},

jQuery.css方法前面几行代码跟style方法一样,也是先格式化样式名,转变成驼峰写法,添加浏览器前缀。这个就不用赘述了。之后是获取相应的hook兼容方法,跟style的代码也一致。

if ( hooks && "get" in hooks ) {
	val = hooks.get( elem, true, extra );
}

// Otherwise, if a way to get the computed value exists, use that
if ( val === undefined ) {
	val = curCSS( elem, name );
}

//convert "normal" to computed value
if ( val === "normal" && name in cssNormalTransform ) {
	val = cssNormalTransform[ name ];
}

// Return, converting to number if forced or a qualifier was provided and val looks numeric
if ( numeric || extra !== undefined ) {
	num = parseFloat( val );
	return numeric || jQuery.isNumeric( num ) ? num || 0 : val;
}
return val;

我们主要看上面的这几个if处理。

第一个if判断如果有hooks兼容的get方法,通过hooks的get获取value值。

第二个if判断如果没有hooks或者第一个hooks得到的是undefined,那么调用curCss方法获取样式值

// NOTE: To any future maintainer, we've window.getComputedStyle
// because jsdom on node.js will break without it.
if ( window.getComputedStyle ) {
	curCSS = function( elem, name ) {
		var ret, width, minWidth, maxWidth,
			computed = window.getComputedStyle( elem, null ),
			style = elem.style;

		if ( computed ) {

			// getPropertyValue is only needed for .css('filter') in IE9, see #12537
			ret = computed.getPropertyValue( name ) || computed[ name ];

			if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) {
				ret = jQuery.style( elem, name );
			}

			// A tribute to the "awesome hack by Dean Edwards"
			// Chrome < 17 and Safari 5.0 uses "computed value" 
			//instead of "used value" for margin-right
			// Safari 5.1.7 (at least) returns percentage for a larger set of values,
			// but width seems to be reliably pixels
			// this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values
			if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) {
				width = style.width;
				minWidth = style.minWidth;
				maxWidth = style.maxWidth;

				style.minWidth = style.maxWidth = style.width = ret;
				ret = computed.width;

				style.width = width;
				style.minWidth = minWidth;
				style.maxWidth = maxWidth;
			}
		}

		return ret;
	};
} else if ( document.documentElement.currentStyle ) {
	curCSS = function( elem, name ) {
		var left, rsLeft,
			ret = elem.currentStyle && elem.currentStyle[ name ],
			style = elem.style;

		// 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
		if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) {

			// Remember the original values
			left = style.left;
			rsLeft = elem.runtimeStyle && elem.runtimeStyle.left;

			// Put in the new values to get a computed value out
			if ( rsLeft ) {
				elem.runtimeStyle.left = elem.currentStyle.left;
			}
			style.left = name === "fontSize" ? "1em" : ret;
			ret = style.pixelLeft + "px";

			// Revert the changed values
			style.left = left;
			if ( rsLeft ) {
				elem.runtimeStyle.left = rsLeft;
			}
		}

		return ret === "" ? "auto" : ret;
	};
}

这一个curCss方法代码比较长,这里先不详细讲解,后文再说,大致意思说一下,就是通过Firefox的"getComputedStyle"和IE的“currentStyle”功能来获取样式计算后的值。获取样式用这个curCss获取的是浏览器计算后的最终值,而前面的“jQuery.style”方法中的“style[name]”只是获取的元素的内联style属性上的样式值,即使是通过css样式文件添加的样式也获取不到,适用面很窄,所以要使用这个“jQuery.css”方法来获取样式值。

第三个if是,如果通过上面两个if获取的样式值是“normal”

cssNormalTransform = {
	letterSpacing: 0,
	fontWeight: 400
},

因为“letterSpacing”,"fontWeigth"的“normal”是默认值,则转换成对应的数值值。

第三个if是用来获取jquery自己定义的样式值,通过“$('div').innerWidth()”,"$('div').outerWidth()"方法获取值时会调用到这里。将获取的宽度或高度值,转换成去除单位纯数字。并且返回这个纯数值的结果。

否则的话,最后就返回获得的样式值。

注意:虽然jQuery.css方法主要用来获取元素的样式值,但是jQuery.style方法也提供了获取元素的样式值。除了上面所说的区别外,这两个函数在调用hooks兼容方法时也是有区别的:

jQuery.style的调用

ret = hooks.get( elem, false, extra )

jQuery.css的调用

val = hooks.get( elem, true, extra );

style方法的第二个参数是false,而css方法的第二个参数是true,这个参数用来表示时候要获取非计算的样式值,为true是计算后的样式值,为false,则只是元素的style对象上样式值。什么时候需要非计算的样式值呢?

比如新创建的元素,在没有添加到文档中时,正获得非计算的样式值,这时候浏览器没有对此元素进行渲染,样式还没有计算,所以只能获得非计算的样式值。

结语

jQuery的css方法的大结构和attr方法的结构是一样的,都是通过hooks的方法来处理一些必要的兼容,通过access方法来提供传值方式。css方法的整体代码逻辑不是很难理解。理解了css方法的源码,我们就知道css的具体兼容方式是使用curCss方法,和一些hooks的兼容处理,要想学习具体的兼容性处理,还得细看这些方法。我将在接下来的文章中,来带领大家一起分析这些兼容性方法的源码,一起从中学习更多的浏览器兼容性知识。



原创文章,转载请注明来自:妹纸前端-www.webfront-js.com.
阅读(2778)
辛苦了,打赏喝个咖啡
微信
支付宝
妹纸前端
妹纸前端工作室 | 文章不断更新中
京ICP备16005385号-1