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
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值