jQuery源码解析(4)—— css样式、定位属性

闲话

原计划是没有这篇博文的,研究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读取写入之前,都会先看是否存在钩子并调用,然后决定是否继续下一步还是直接返回。通过在setget属性中定义函数,使得行为正确一致。

扩展

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" ) {
                value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" );
            }

            // Fixes #8908, it can be done more correctly by specifing setters in cssHooks,
            // but it would mean to define eight
            // (for every problematic property) identical functions
            if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) {
                style[ name ] = "inherit";
            }

            // 有钩子先使用钩子,看返回值是否为undefined决定是否style[ name ]赋值,否则直接赋值
            if ( !hooks || !( "set" in hooks ) ||
                ( value = hooks.set( elem, value, extra ) ) !== undefined ) {

                // Support: IE
                // Swallow errors from 'invalid' CSS values (#5509)
                try {
                    style[ name ] = value;
                } catch ( e ) {}
            }

        /**
         *  ---- 3、elem.style方式 - 取值 ---- 
         */
        } else {

            // 有钩子先使用钩子,看返回值是否为undefined决定是否style[ name ]取值,否则直接取值
            if ( hooks && "get" in hooks &&
                ( ret = hooks.get( elem, false, extra ) ) !== undefined ) {

                return ret;
            }

            return style[ name ];
        }
    },

    // 默认computedStyle/currentStyle方式只读,也可styles指定读取对象
    css: function( elem, name, extra, styles ) {

        /**
         * ---- 1、name修正,属性兼容(同style) ---- 
         */
        var num, val, hooks,
            origName = jQuery.camelCase( name );

        name = jQuery.cssProps[ origName ] ||
            ( jQuery.cssProps[ origName ] = vendorPropName( origName ) || origName );

        // 钩子
        hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];

        // 若有钩子,通过钩子读取
        if ( hooks && "get" in hooks ) {
            val = hooks.get( elem, true, extra );
        }

        // 没有钩子,通过封装的curCSS读取
        if ( val === undefined ) {
            val = curCSS( elem, name, styles );
        }

        // 属性值为"normal",若为cssNormalTransform内的属性,把对应值输出
        if ( val === "normal" && name in cssNormalTransform ) {
            val = cssNormalTransform[ name ];
        }

        // extra === "" 去单位处理,若为"normal"、"auto"等字符串,原样返回
        // extra === true 强制去单位,若为parseFloat后为NaN的字符串,返回0
        // extra === false/undefined 不特殊处理
        if ( extra === "" || extra ) {
            num = parseFloat( val );
            return extra === true || isFinite( num ) ? num || 0 : val;
        }
        return val;
    }
} );


5、adjsutCSS换算

adjustCSS( elem, prop, valueParts, tween )用于调用jQuery.style对增量计算的换算,并得到最终值。在jq内部,除了css样式会换算,动画处理也支持换算。这里也可以把动画的tween对象的初始值和增量进行累加换算,得到最终值赋给tween对象

难点:
这里需要知道jQuery.css( elem, prop, "" )通过computedStyle/currentStyle求得的值单位不变,并且被extra=”“去掉了单位。比如初始值是30px,增量为’+=1rem’,先使用增量的单位30rem,然后调用jQuery.css查询跟修改前的初始值比较,比如变成了scale=15倍,则30rem/15=2rem求得原值换算后为2rem,然后再累加返回3rem。
maxIterations设为20有两个原因:1、js浮点误差可能导致两边总是不相等;2、对于首次调整单位变成了很小的倍数趋近于0无法计算,则通过重置为0.5每次乘2直到可以计算,慢慢的调整差距

bug(建议见第4点):
cssNumber列表中属性的值使用无单位增量如’+=10’,而初始值单位为px,将按照初始值单位’+=10px’处理后返回。但返回到外部,由于在cssNumber列表中,并不会再次加上单位,按照倍数被设置了。
比如lineHeight初始值20px,使用’+=4’,变成了赋值24倍

// valueParts为增量匹配结果集,两种形式
// 动画 adjustCSS(tween.elem, prop, rcssNum.exec( value ), tween)
// css adjustCSS(elem, prop, rcssNum.exec( value ))
function adjustCSS( elem, prop, valueParts, tween ) {
    var adjusted,
        // 默认比例
        scale = 1,
        // 最大修正次数
        maxIterations = 20,
        currentValue = tween ?
            // 动画对象当前属性值计算
            function() { return tween.cur(); } :
            function() { return jQuery.css( elem, prop, "" ); },
        // 当前用作累加基数的初始值
        initial = currentValue(),
        // 匹配单位,若不在cssNumber目录,并且没带单位,则当做px
        unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),

        // 由于初始值若匹配到单位,都会是px,不是的在执行css过程中jq也有钩子修正,所以有可能需要换算的只有cssNumber列表中项目,或者unit不为px且initial有非0数值的(0无需换算)。初始值为字符串如"auto",则会在下面按照0处理
        initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) &&
            rcssNum.exec( jQuery.css( elem, prop ) );

    // 单位不同时换算
    if ( initialInUnit && initialInUnit[ 3 ] !== unit ) {

        // 默认使用增量值单位
        // 若为cssNumber中属性,且增量无单位,则使用初始值单位,后面也无需换算了
        // 小bug:cssNumber列表中属性'+=10'若无unit,按照初始值单位'+=10px'处理返回。但返回到外部,由于在cssNumber列表中,并不会再次加上单位,按照倍数被设置了。比如lineHeight初始值20px,使用'+=4',变成了赋值24倍
        unit = unit || initialInUnit[ 3 ];

        // Make sure we update the tween properties later on
        valueParts = valueParts || [];

        // Iteratively approximate from a nonzero starting point
        // 此处个人觉得没有 || 1 的写法没有必要性,若为0,则无需换算了
        initialInUnit = +initial || 1;

        // 换算,见难点解释
        do {

            // If previous iteration zeroed out, double until we get *something*.
            // Use string for doubling so we don't accidentally see scale as unchanged below
            scale = scale || ".5";

            // Adjust and apply
            initialInUnit = initialInUnit / scale;
            jQuery.style( elem, prop, initialInUnit + unit );

        // Update scale, tolerating zero or NaN from tween.cur()
        // Break the loop if scale is unchanged or perfect, or if we've just had enough.
        } while (
            scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations
        );
    }

    if ( valueParts ) {
        // 初始值为字符串,也将按照0处理,需要注意咯
        initialInUnit = +initialInUnit || +initial || 0;

        // 根据是否为增量运算判断直接赋值还是换算后的初始值与增量相加,css运算中只允许增量运算使用该函数
        adjusted = valueParts[ 1 ] ?
            initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] :
            +valueParts[ 2 ];
        // 对动画对象赋初值和末值
        if ( tween ) {
            tween.unit = unit;
            tween.start = initialInUnit;
            tween.end = adjusted;
        }
    }
    return adjusted;
}


6、curCSS、getStyles

curCSS( elem, name, computed )是对getStyles( elem )的封装,可以通过computed指定样式对象替代内部的getStyle。对高版本浏览器和低版本IE getStyle分别使用的getComputedStyle、currentStyle,前者是全局对象下的属性,所以源码中使用了ownerDocument.defaultView指代。

不同的浏览器,对属性的返回可能出现百分比等非px返回值,jq通过钩子处理个体,curCSS内部也处理了一些情况,比如Chrome、Safari的margin相关属性值返回百分比,低版本IE的非top等位置属性返回百分比等。

// #6489,下面会用到的正则
var rmargin = ( /^margin/ );
var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" );

// #6692
var getStyles, curCSS,
    rposition = /^(top|right|bottom|left)$/;

// 高版本浏览器
if ( window.getComputedStyle ) {
    getStyles = function( elem ) {

        // Support: IE<=11+, Firefox<=30+ (#15098, #14150)
        // IE throws on elements created in popups
        // FF meanwhile throws on frame elements through "defaultView.getComputedStyle"
        var view = elem.ownerDocument.defaultView;

        // opener指的是打开该页面的源页面的window
        if ( !view || !view.opener ) {
            view = window;
        }

        return view.getComputedStyle( elem );
    };

    // 默认使用getStyles,也可通过computed参数指定样式对象。内部还有对文档片段和margin类属性值的特殊处理
    curCSS = function( elem, name, computed ) {
        var width, minWidth, maxWidth, ret,
            style = elem.style;

        computed = computed || getStyles( elem );

        // getPropertyValue is only needed for .css('filter') in IE9, see #12537
        // getComputedStyle(elem).getPropertyValue(name)其实也可以用来获取属性,但是不支持驼峰,必须-连接书写,否则返回""
        ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined;

        // 文档片段document fragments的元素通过getComputedStyle取样式是""或undefined,需要退回到style方式取
        // 文档片段中的元素elem.ownerDocument不为文档片段,为docuemnt
        if ( ( ret === "" || ret === undefined ) && !jQuery.contains( elem.ownerDocument, elem ) ) {
            ret = jQuery.style( elem, name );
        }

        if ( computed ) {

            // 为了兼容有的浏览器margin相关方法返回百分比等非px值的情况,由于width输出是px,并且margin的百分比是按照width计算的,因此可以直接赋值width。设置minWidth/maxWidth是为了保证设置的width不会因为超出限制失效
            if ( !support.pixelMarginRight() && rnumnonpx.test( ret ) && rmargin.test( name ) ) {

                // 记忆
                width = style.width;
                minWidth = style.minWidth;
                maxWidth = style.maxWidth;

                // 把margin的值设置到width,并获取对应width值作为结果
                style.minWidth = style.maxWidth = style.width = ret;
                ret = computed.width;

                // 还原
                style.width = width;
                style.minWidth = minWidth;
                style.maxWidth = maxWidth;
            }
        }

        // Support: IE
        // IE returns zIndex value as an integer.都以字符串返回
        return ret === undefined ?
            ret :
            ret + "";
    };

// IE 6-8
} else if ( documentElement.currentStyle ) {
    getStyles = function( elem ) {
        return elem.currentStyle;
    };

    curCSS = function( elem, name, computed ) {
        var left, rs, rsLeft, ret,
            style = elem.style;

        computed = computed || getStyles( elem );
        ret = computed ? computed[ name ] : undefined;

        // 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
        // 对非位置top|left|right|bottom返回的,先把left属性保存,然后把属性设置到left上,然后取出
        if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) {

            // 记忆
            left = style.left;
            // runtimeStyle是低版本IE中表示运行中样式,可读可写,优先级大于style设置的
            rs = elem.runtimeStyle;
            rsLeft = rs && rs.left;

            // Put in the new values to get a computed value out
            if ( rsLeft ) {
                rs.left = elem.currentStyle.left;
            }
            // 对于百分比,是以父元素宽度为基准。而对于fontSize,设置到left的1rem大小则是固定的
            style.left = name === "fontSize" ? "1em" : ret;
            ret = style.pixelLeft + "px";

            // 还原
            style.left = left;
            if ( rsLeft ) {
                rs.left = rsLeft;
            }
        }

        // Support: IE
        // IE returns zIndex value as an integer.数字以字符串形式返回
        return ret === undefined ?
            ret :
            ret + "" || "auto";
    };
}


7、cssHooks钩子

正如第4点提到的,cssHooks存在的主要目的是应对存取出现的不一致行为。jq用support对象放置测试后的兼容性信息,源码#6519 - #6690行有很多support对象的样式方面兼容测试,内部通过addGetHookIf( conditionFn, hookFn )来绑定钩子。钩子有个computed参数,用于标记是jQuery.css/style哪个方法的读操作触发的,对应true/false

存:存的不一致只有一个

1、opacity透明度对于IE内部使用filter,需要设置为alpha(opacity=100*value)的形式
*、对于padding、borderWidth、height、width的存只是做了负数变为0的特殊处理。

取:取的不一致比较多

1、opacity的”“按”1”处理,对于需要使用filter的IE低版本也要hook
2、height、width的获取需要display不为none和带有table的任意值(除了table、table-cell、table-caption三样),因此提供了swap( elem, options, callback, args )用于以指定属性状态调用函数取值,之后还原状态
3、marginLeft、marginRight对于不支持返回可靠值的浏览器做处理,marginRight在display为”inline-block”下取值,marginLeft通过getBoundingClientRect比对与marginLeft设置为0后的位置差得到
4、top、left中有可能返回百分比的浏览器,先取值,若不为px单位,则调用内部position方法计算top、left(但是此方法是相对有定位父集或html的,对于position为relative的是有bug的,个人建议源码中可以对relative的使用getBoundingClientRect比对处理)

扩展: innerWidth()/innerHeight()/outerWidth()/outerHeight()

盒模型默认是宽高不包括padding、border、margin。css3里有boxSizing属性,content-box|border-box|inherit分别代表 “不包括padding、border、margin” | “包含border和padding” | “继承”。

jq通过innerWidth()/innerHeight()可以直接查询/设置content-box区域的长宽;通过outerWidth()/outerHeight()可查询/设置为border-box区域的长宽,增加一个参数true,如([value, ]true),可查询/设置为border-box区域加上margin区域的总长宽。

jq仍然是设置height、width,不过它会进行换算。通过augmentWidthOrHeight( elem, name, extra, isBorderBox, styles )计算增量(数值),通过getWidthOrHeight( elem, name, extra )得到最终值(带px字符串)。通过extra来指明按照content、padding、border、margin中哪一个级别。

注意
cssHooks内若要得到自身属性的样式,不调用jQuery.css,而是直接调用curCSS,包括getWidthOrHeight内,因为curCSS是纯粹的取值,不会调用钩子造成死循环

/* #1307 contains
 * 节点包含。后面经常用来验证是否为文档片段中的元素
---------------------------------------------------------------------- */
// /^[^{]+\{\s*\[native \w/  -> 匹配内部方法 'funtion xxx() { [native code] }'
hasCompare = rnative.test( docElem.compareDocumentPosition );

// 返回布尔值,true表示 b节点在a节点内/a文档的根节点内(节点相等为false)
// ie9+及其他浏览器支持compareDocumentPosition,ie6-8支持contains,比较老的safari都不支持使用下面的函数
contains = hasCompare || rnative.test( docElem.contains ) ?
    function( a, b ) {
        var adown = a.nodeType === 9 ? a.documentElement : a,
            bup = b && b.parentNode;
        return a === bup || !!( bup && bup.nodeType === 1 && (
            adown.contains ?
                adown.contains( bup ) :
                // a.compareDocumentPosition( bup ) = 16表示 a包含b
                a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
        ));
    } :
    function( a, b ) {
        if ( b ) {
            // 若不等于父节点,继续冒泡知道根节点
            while ( (b = b.parentNode) ) {
                if ( b === a ) {
                    return true;
                }
            }
        }
        return false;
    };


/* #6494 swap
 * elem的style在options状态下调用callback.apply(elem, args),然后改回原属性。返回查到的值
---------------------------------------------------------------------- */
var swap = function( elem, options, callback, args ) {
    var ret, name,
        old = {};

    // 记录原有值,设定上新值
    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;
};

/* #6816 addGetHookIf
 * conditionFn()执行后返回true,说明支持,不会绑定hookFn钩子
---------------------------------------------------------------------- */
function addGetHookIf( conditionFn, hookFn ) {

    // Define the hook, we'll check on the first run if it's really needed.
    // 预执行和懒加载的方式均可,源码选择了懒加载。第一次当做钩子执行调用时绑定真实钩子或删除
    return {
        get: function() {
            if ( conditionFn() ) {

                // 支持,无需钩子
                delete this.get;
                return;
            }

            // 需要钩子,定义为hookFn。即使是第一次也要执行一次
            return ( this.get = hookFn ).apply( this, arguments );
        }
    };
}


/* #6935 setPositiveNumber
 * 保证非负值,保留单位,subtract可以指定需要减去的值
---------------------------------------------------------------------- */
function setPositiveNumber( elem, value, subtract ) {
    var matches = rnumsplit.exec( value );
    return matches ?

        // Guard against undefined "subtract", e.g., when used as in cssHooks
        Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) :
        value;
}

/* #6944 augmentWidthOrHeight
 * 根据extra类型计算增量(相对于height/width取值),返回纯数值
 * 注意:读取为增量,写入为减量
---------------------------------------------------------------------- */
function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {

    // border-box -> border, content-box -> content。无需修正为4
    var i = extra === ( isBorderBox ? "border" : "content" ) ?

        // If we already have the right measurement, avoid augmentation
        4 :

        // height: 0(top) 2(bottom)  width: 1(right) 3(left)
        // cssExpand = [ "Top", "Right", "Bottom", "Left"];
        name === "width" ? 1 : 0,

        val = 0;

    for ( ; i < 4; i += 2 ) {

        // border-box  content-box 想变为margin级别都需要 + margin值
        if ( extra === "margin" ) {
            val += jQuery.css( elem, extra + cssExpand[ i ], true, styles );
        }

        if ( isBorderBox ) {

            // border-box = content级别 + "padding" + "border"(下面那个)
            if ( extra === "content" ) {
                // true 表示强制去单位
                val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
            }

            // border-box = padding级别 + "border"
            if ( extra !== "margin" ) {
                val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
            }
        } else {

            // 逻辑能走到这里,说明一定不是content级别,否则 i = 4
            // content-box 变为任意级别都要 + padding 。 true 表示强制去单位
            val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );

            // 变为border级别、margin级别要 + border
            if ( extra !== "padding" ) {
                val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
            }
        }
    }

    return val;
}

/* #6988 getWidthOrHeight
 * 用于读取,会根据extra加上augmentWidthOrHeight增量
---------------------------------------------------------------------- */
function getWidthOrHeight( elem, name, extra ) {

    // getWidthOrHeight = contentBox级别值 + augmentWidthOrHeight增量
    // 这里直接用offsetWidth/offsetHeight返回的borderbox级别值作为基础值,因此下面需要调整,valueIsBorderBox默认值为true,表示为border-box
    var valueIsBorderBox = true,
        val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
        styles = getStyles( elem ),
        // 只有支持boxSizing属性,且为border-box,isBorderBox才为true,否则要调整val
        isBorderBox = support.boxSizing &&
            jQuery.css( elem, "boxSizing", false, styles ) === "border-box";

    // Support: IE11 only,全屏浏览下bug,不了解(逃
    // In IE 11 fullscreen elements inside of an iframe have
    // 100x too small dimensions (gh-1764).
    if ( document.msFullscreenElement && window.top !== window ) {

        // Support: IE11 only
        // Running getBoundingClientRect on a disconnected node
        // in IE throws an error.
        if ( elem.getClientRects().length ) {
            val = Math.round( elem.getBoundingClientRect()[ name ] * 100 );
        }
    }

    // some non-html elements return undefined for offsetWidth, so check for null/undefined
    // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
    // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668

    // svg 和 MathML 可能会返回 undefined,需要重新求值
    if ( val <= 0 || val == null ) {

        // 直接获取 width/height 作为基础值,若之后调用elem.style,说明support.boxSizingReliable()一定为false
        val = curCSS( elem, name, styles );
        if ( val < 0 || val == null ) {
            val = elem.style[ name ];
        }

        // 匹配到非px且带单位的值,则直接退出
        if ( rnumnonpx.test( val ) ) {
            return val;
        }

        // valueIsBorderBox意思是得到的value是borderbox级别的,由于调整为了curCSS取值,因此,必须要isBorderBox为true,不可靠值当做content级别处理(因为border、padding容易获取到准确值,val === elem.style[ name ]除外)
        valueIsBorderBox = isBorderBox &&
            ( support.boxSizingReliable() || val === elem.style[ name ] );

        // Normalize "", auto, and prepare for extra
        // 强制去单位,"auto"等字符串变为0
        val = parseFloat( val ) || 0;
    }

    // use the active box-sizing model to add/subtract irrelevant styles
    return ( val +
        augmentWidthOrHeight(
            elem,
            name,
            // 若没指定,默认值跟盒模型一致
            extra || ( isBorderBox ? "border" : "content" ),
            // 表示基数val是否为borderBox,extra和它一致说明无需累加
            valueIsBorderBox,
            styles
        )
    ) + "px";
}



/* #7201 cssHooks[ "height", "width" ]
 * 防止设置负数。支持根据extra指定级别修正设定/获取值
---------------------------------------------------------------------- */
jQuery.each( [ "height", "width" ], function( i, name ) {
    jQuery.cssHooks[ name ] = {
        get: function( elem, computed, extra ) {
            // innerWidth等API内部只调用jQuery.css,style方式不用钩子,所以false则退出
            if ( computed ) {

                // rdisplayswap = /^(none|table(?!-c[ea]).+)/
                // cssShow = { position: "absolute", visibility: "hidden", display: "block" }
                // display影响了定位信息的获取,比如offsetWidth为0。先设置cssShow属性获取到值,然后改回属性
                return rdisplayswap.test( jQuery.css( elem, "display" ) ) &&
                    elem.offsetWidth === 0 ?
                        swap( elem, cssShow, function() {
                            return getWidthOrHeight( elem, name, extra );
                        } ) :
                        getWidthOrHeight( elem, name, extra );
            }
        },

        set: function( elem, value, extra ) {
            var styles = extra && getStyles( elem );
            // 设置非负值,在设置时增量即为减量,第三个参数对于substract参数
            return setPositiveNumber( elem, value, extra ?
                augmentWidthOrHeight(
                    elem,
                    name,
                    extra,
                    support.boxSizing &&
                        jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
                    styles
                ) : 0
            );
        }
    };
} );

/* #7053 #7233 cssHooks[ "opacity" ]
 * 根据是否支持opacity,判断内部使用opacity还是filter
---------------------------------------------------------------------- */
cssHooks: {
    opacity: {
        get: function( elem, computed ) {
            // elem.style['opacity']调用无需钩子,所以false则不处理
            if ( computed ) {

                // We should always get a number back from opacity
                var ret = curCSS( elem, "opacity" );
                return ret === "" ? "1" : ret;
            }
        }
    }
},

// #7233 filter部分
if ( !support.opacity ) {
    jQuery.cssHooks.opacity = {
        // computed -> css(true)、style(false) 均hook
        get: function( elem, computed ) {

            // IE uses filters for opacity
            // ropacity = /opacity\s*=\s*([^)]*)/i,
            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 === "", then remove inline opacity #12685
            if ( ( value >= 1 || value === "" ) &&
                    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 is no filter style applied in a css rule
                // or unset inline opacity, we are done
                if ( value === "" || currentStyle && !currentStyle.filter ) {
                    return;
                }
            }

            // otherwise, set new filter values
            // ralpha = /alpha\([^)]*\)/i
            style.filter = ralpha.test( filter ) ?
                filter.replace( ralpha, opacity ) :
                filter + " " + opacity;
        }
    };
}

/* #7282 cssHooks[ "marginRight" "marginLeft" ]
 * 只对不支持的浏览器使用 get 钩子,通过addGetHookIf
---------------------------------------------------------------------- */
jQuery.cssHooks.marginRight = addGetHookIf( support.reliableMarginRight,
    function( elem, computed ) {
        // 仅css方式
        if ( computed ) {
            return swap( elem, { "display": "inline-block" },
                curCSS, [ elem, "marginRight" ] );
        }
    }
);

jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,
    function( elem, computed ) {
        // 仅css方式
        if ( computed ) {
            return (
                parseFloat( curCSS( elem, "marginLeft" ) ) ||

                // Support: IE<=11+
                // Running getBoundingClientRect on a disconnected node in IE throws an error
                // Support: IE8 only
                // getClientRects() errors on disconnected elems
                ( jQuery.contains( elem.ownerDocument, elem ) ?
                    // 与marginLeft=0的left坐标比对差值
                    elem.getBoundingClientRect().left -
                        swap( elem, { marginLeft: 0 }, function() {
                            return elem.getBoundingClientRect().left;
                        } ) :
                    // 文档片段中的元素按0处理
                    0
                )
            ) + "px";
        }
    }
);

/* #10882 cssHooks[ "top" "left" ]
 * 只对不支持px返回位置的浏览器使用 get 钩子,通过addGetHookIf
---------------------------------------------------------------------- */
// 
jQuery.each( [ "top", "left" ], function( i, prop ) {
    jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,
        function( elem, computed ) {
            if ( computed ) {
                computed = curCSS( elem, prop );

                // if curCSS returns percentage, fallback to offset
                // position是相对最近的有定位的祖先节点的偏移,对于position:relative是不合适的,算是个bug吧
                return rnumnonpx.test( computed ) ?
                    jQuery( elem ).position()[ prop ] + "px" :
                    computed;
            }
        }
    );
} );


8、innerWidth/Height()、outerWidth/Height()

第7点已经讲了很多,增量的处理,并不在这几个函数中,统一交给了augmentWidthOrHeight,使得逻辑变得简单统一,很好的实践了分离可变与不变的思想。

注意:它们不受jQuery.style中的小bug(如200有效,’200’无效)影响。它们不在cssNumber列表,虽然不会再该函数里为字符串自动补px,但是钩子中的setPositiveNumber是自动补单位px输出的。


// #269
jQuery.isWindow = function( obj ) {
    /* jshint eqeqeq: false */
    return obj != null && obj == obj.window;
};

// #10696
function getWindow( elem ) {
    return jQuery.isWindow( elem ) ?
        elem :
        elem.nodeType === 9 ?
            elem.defaultView || elem.parentWindow :
            false;
}

// #10893
jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
    jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name },

    // funcName代表innerWidth、innerHeight、outerWidth、outerHeight
    function( defaultExtra, funcName ) {

        // 参数 ( [ margin [,value] ] ),margin才是数值
        // 正确使用 $().outerHeight(值, true)
        jQuery.fn[ funcName ] = function( margin, value ) {
            // defaultExtra有值,说明是padding和content的情况,只要有参数说明链式
            // defaultExtra为"",说明是margin和border情况,只有首参数有值并不为boolean,则链式
            var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
                // 确定级别
                extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );

            // 第1类access用法
            return access( this, function( elem, type, value ) {
                var doc;

                if ( jQuery.isWindow( elem ) ) {

                    // "clientHeight"  "clientWidth" 可视区大小
                    return elem.document.documentElement[ "client" + name ];
                }

                // Get document width or height
                if ( elem.nodeType === 9 ) {
                    doc = elem.documentElement;

                    // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],
                    // whichever is greatest
                    // unfortunately, this causes bug #3838 in IE6/8 only,
                    // but there is currently no good, small way to fix it.
                    return Math.max(
                        elem.body[ "scroll" + name ], doc[ "scroll" + name ],
                        elem.body[ "offset" + name ], doc[ "offset" + name ],
                        doc[ "client" + name ]
                    );
                }

                // 此处value -> chainable ? margin : undefined
                return value === undefined ?

                    // extra传递级别,并且不强制的去除单位
                    jQuery.css( elem, type, extra ) :

                    // Set width or height on the element
                    jQuery.style( elem, type, value, extra );
            }, type, chainable ? margin : undefined, chainable, null );
        };
    } );
} );


9、display与jQuery.fn.show/hide/toggle

与jQuery.fn.css()在一起定义的还有jQuery.fn.show/hide/toggle()方法,通过display:none影响元素的可见性。如同swap函数会记录样式并还原一样,为了让隐藏还原为可见的过程中display与隐藏前保持一致,把display的原状态存入jQuery._data(elem, “olddisplay”)缓存,对于没有缓存的显示的时候会调用defaultDisplay( nodeName )首先查询elemdisplay列表中是否有默认值,若没有,内部调用actualDisplay( name, doc )通过创建一个相同标签检测display得到需要设置的display值(结果会添加到elemdisplay列表加快以后查询),从而保证正确的显示状态。这一切的核心过程就是showHide()

showHide()这个函数跟jq中很多函数一样,很污!合并大量分支条件,就是不肯多写两行代码的臭毛病,原谅我年轻,赶脚是在炫技!!

要点:
1、设置为hide,都是通过elem.style.display= none。缓存显示状态到_data(已经有缓存值则不需要)
2、当调整为显示时使用缓存值。缓存值一定是最终可靠值,尽管信任
3、这种逻辑关系建立在只通过showHide()机制控制是否显示,这样记忆的值才能真正代表上一次

show 变量代表变为显示状态,hidden 变量代表本身是隐藏
show(设置显示) -> hidden(本身隐藏) :调整为_data缓存中保存的用于显示的设置;若无缓存,先把display设置为”“,若此时检测仍为hidden,则设置display到元素初始默认,并缓存起来(对文档片段也是友好的)
show(设置显示) -> !hidden(本身显示) :不做处理。下面会路过elem.style.display === “” -> values[ index ] || “” ,相当于没处理
!show(设置隐藏)-> hidden(本身隐藏) :display && display !== “none”,不为”“但仍隐藏,说明这段代码是为了兼容文档片段的,会缓存elem.style.display。最终display设为none
!show(设置隐藏)-> !hidden(本身显示) :缓存css取到的生效样式,因为style的可能为”“,最终display设为none

/* #4304 isHidden
 * 对display:none,或者文档片段中的节点,返回true
---------------------------------------------------------------------- */
var isHidden = function( elem, el ) {
    elem = el || elem;
    return jQuery.css( elem, "display" ) === "none" ||
        !jQuery.contains( elem.ownerDocument, elem );
};

// #6427  默认display值列表
var iframe,
    elemdisplay = {

        // Support: Firefox
        // We have to pre-define these values for FF (#10227)
        HTML: "block",
        BODY: "block"
    };

/* #6443 actualDisplay
 * 创建时默认生效的display
---------------------------------------------------------------------- */
function actualDisplay( name, doc ) {
    var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ),

        display = jQuery.css( elem[ 0 ], "display" );

    // #6251  删除该节点
    elem.detach();

    return display;
}

/* #6459 defaultDisplay
 * 创建时默认生效的display
---------------------------------------------------------------------- */
function defaultDisplay( nodeName ) {
    var doc = document,
        display = elemdisplay[ nodeName ];

    if ( !display ) {
        display = actualDisplay( nodeName, doc );

        // If the simple way fails, read from inside an iframe
        // 失效了,就在一个iframe再试一次
        if ( display === "none" || !display ) {

            // Use the already-created iframe if possible
            iframe = ( iframe || jQuery( "