jQuery-1.9.1源码分析系列(七) 钩子(hooks)机制及浏览器兼容续
前面一章分析了jQuery.support、钩子原理和属性钩子。这一章主要分析CSS钩子。
b. css操作的钩子
CSS钩子种类:
cssHooks
cssNumber
cssProps
jQuery.cssHooks的对象
不过cssHooks中的set函数的作用有些不同,set函数并没有真正的设置相应的值,而是修正要设置到CSS中的值。获取到修正值以后,设置在jQuery.style函数中进行。后面分析几个CSS钩子
获取opacity返回的值需要时数字
cssHooks: { opacity: { get: function( elem, computed ) { if ( computed ) { //需要返回数字 var ret = curCSS( elem, "opacity" ); return ret === "" ? "1" : ret; } } } },
当设置display为none等的时候是不能获取到宽高的,所以需要将元素设为display为block,visibility设置为hidden来获取宽高;设置宽高需要根据CSS样式boxSizing的取值来确定。
jQuery.each([ "height", "width" ], function( i, name ) { jQuery.cssHooks[ name ] = { get: function( elem, computed, extra ) { if ( computed ) { //当设置display为none等的时候是不能获取到宽高的, //所以需要将元素设为display为block, //visibility设置为hidden来获取宽高 // rdisplayswap = /^(none|table(?!-c[ea]).+)/, //cssShow ={ position: "absolute", visibility: "hidden", display: "block" } return elem.offsetWidth === 0 && rdisplayswap.test( jQuery.css( elem, "display" ) ) ? jQuery.swap( elem, cssShow, function() { return getWidthOrHeight( elem, name, extra ); }) : getWidthOrHeight( elem, name, extra ); } }, set: function( elem, value, extra ) { var styles = extra && getStyles( elem ); return setPositiveNumber( elem, value, extra ? augmentWidthOrHeight(elem, name, extra, jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box",styles) : 0 ); } }; });
IE使用filters来设置不透明度;IE要设置不透明度更加复杂,需保证有布局,如果设置不透明度为1,并且没有别的filters存在,尝试移除filter属性等
if ( !jQuery.support.opacity ) { jQuery.cssHooks.opacity = { get: function( elem, computed ) { // IE使用filters来设置不透明度,ropacity = /opacity\s*=\s*([^)]*)/ 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 || ""; //IE7中滤镜(filter)必须获得布局才能生效,我们用zoom这个IE私有属性让其获得布局 style.zoom = 1; //ie处理,如果设置不透明度为1,并且没有别的filters存在,尝试移除filter属性 //如果只为“”,删除内联的opacity,ralpha = /alpha\([^)]*\)/i if ( ( value >= 1 || value === "" ) && jQuery.trim( filter.replace( ralpha, "" ) ) === "" && style.removeAttribute ) { //设置style.filter为null, "" 或 " ",结果是"filter:"依然在cssText中 //如果当下"filter:"存在则清除类型不可用,我们应当避免 // style.removeAttribute是IE独有 style.removeAttribute( "filter" ); //如果当前没有filter样式应用于css rule或未设置内联的不透明则返回 if ( value === "" || currentStyle && !currentStyle.filter ) { return; } } // 其他情况设置filter values style.filter = ralpha.test( filter ) ? filter.replace( ralpha, opacity ) : filter + " " + opacity; } }; }
webkit的bug:getComputedStyle返回margin-right值错误;当指定为top/left/bottom/right时,使用getComputedStyle 返回百分比结果,使用jQuery( elem ).position()来获取。
//DOM加载完成后才能做support测试,在添加下面的Hooks jQuery(function() { if ( !jQuery.support.reliableMarginRight ) { jQuery.cssHooks.marginRight = { get: function( elem, computed ) { if ( computed ) { // WebKit Bug 13343 - getComputedStyle返回margin-right值错误 //设置元素的display为inline-block来解决 return jQuery.swap( elem, { "display": "inline-block" }, curCSS, [ elem, "marginRight" ] ); } } }; } // Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084 // getComputedStyle 返回百分比当指定为top/left/bottom/right //我们使用jQuery( elem ).position()来获取 if ( !jQuery.support.pixelPosition && jQuery.fn.position ) { jQuery.each( [ "top", "left" ], function( i, prop ) { jQuery.cssHooks[ prop ] = { get: function( elem, computed ) { if ( computed ) { computed = curCSS( elem, prop ); // if curCSS returns percentage, fallback to offset //rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ), return rnumnonpx.test( computed ) ? jQuery( elem ).position()[ prop ] + "px" : computed; } } }; }); } });
后面这个钩子与众不同,他使用在动画的展开上。我们知道margin/padding/borderWidth实际上不是一个CSS属性,而是四个CSS属性的集合。所以三个CSS的expand钩子实际上是将他们拆分成四个属性给提取出来。
// 这里的hooks用在动画的展开特征上 jQuery.each({ margin: "", padding: "", border: "Width" }, function( prefix, suffix ) { jQuery.cssHooks[ prefix + suffix ] = { expand: function( value ) { var i = 0, expanded = {}, //如果不是字符串则假设为一个单独的数字 parts = typeof value === "string" ? value.split(" ") : [ value ]; for ( ; i < 4; i++ ) { //cssExpand = [ "Top", "Right", "Bottom", "Left" ], expanded[ prefix + cssExpand[ i ] + suffix ] = parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; } return expanded; } }; if ( !rmargin.test( prefix ) ) { jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; } });
jQuery. cssNumber和jQuery.cssProps的对象
//下面的css特征值后面不能添加“px”字段 cssNumber: { "columnCount": true, "fillOpacity": true, "fontWeight": true, "lineHeight": true, "opacity": true, "orphans": true, "widows": true, "zIndex": true, "zoom": true }, //float对应的css特征名需要在使用前修正 cssProps: { // normalize float css property "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" },
拓展:
有一些CSS属性需要在某些特定条件下才能获取正确。这种情况需要模拟场景获取值,然后恢复先前的场景。swap就是用来专门模拟场景,获取值以后恢复场景的函数。
// 一种快速切换输入/输出css特征值(计算前保存css特征,计算中更改css特征以获取计算结果,计算结束后恢复先前保存的css特征)以获取正确计算结果的方法 swap: function( elem, options, callback, args ) { var ret, name, old = {}; // 保存原来的特征值,设置为保证计算成功而修改的特征(property)值 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; }
setPositiveNumber函数对要设置给CSS属性的值做修正,比如添加"px"结尾等
// rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ), //用来匹配数字,.source返回表达式字符串自身 //core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, //返回指定value和subtract对应应该设置的css样式值 function setPositiveNumber( elem, value, subtract ) { var matches = rnumsplit.exec( value ); return matches ? // 注意没有定义的"subtract",例如在cssHooks中使用时 Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : value; }
getWidthOrHeight函数提供获取CSS宽高属性的计算方法
//获取宽度或高度 function getWidthOrHeight( elem, name, extra ) { //首先获取offset特征值,相当于包括边框在内的盒宽高 var valueIsBorderBox = true, val = name === "width" ? elem.offsetWidth : elem.offsetHeight, styles = getStyles( elem ), isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; //一些非html元素offsetWidth返回undefined,因此检查null/undefined if ( val <= 0 || val == null ) { //如果计算失败则在必要的情况下使用未计算的结果 val = curCSS( elem, name, styles ); if ( val < 0 || val == null ) { val = elem.style[ name ]; } //已计算的单元为非像素单位则终止并返回 if ( rnumnonpx.test(val) ) { return val; } //我们需要检查style,避免浏览器使用getComputedStyle返回不可靠的值而悄悄的回到可靠的elem.style值 valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] ); //规范“”,auto为拓展做准备 val = parseFloat( val ) || 0; } //使用动态box-sizing模型来添加/减少不相干的样式 return ( val + augmentWidthOrHeight( elem, name, extra || ( isBorderBox ? "border" : "content" ), valueIsBorderBox, styles ) ) + "px"; } //extra表示计算时要包括的部分 function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { //在循环中遍历cssExpand时使用 //其中cssExpand = [ "Top", "Right", "Bottom", "Left" ], var i = extra === ( isBorderBox ? "border" : "content" ) ? //如果我们有了正确的测量结果,避免增大,正常情况下会走这一步 4 : //否则初始化为水平或垂直特征(property) name === "width" ? 1 : 0, val = 0; for ( ; i < 4; i += 2 ) { //两种盒模型都排除margin,如果计算要包括margin,则加上他 if ( extra === "margin" ) { val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); } //如果是border-box模型 if ( isBorderBox ) { // border-box包括padding,如果我们需要内容部分因此要减去他 if ( extra === "content" ) { val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); } //此时,extra不是边框也非margin时,减去边框 if ( extra !== "margin" ) { val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); } //如果是content-box模型 } else { //此时,extra不是内容,所以加上padding val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); //此时,extra不是内容也不是padding,所以加上边框 if ( extra !== "padding" ) { val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); } } } return val; }