jQuery源码分析(十九): DOM操作之样式操作

前端语法/样式/布局 2016-12-09

起步

我们基本都是通过jq的 .css() 来设置样式的。原生的js是这样设置的:

var head= document.getElementById("head");
head.style.width = "20px";
head.style.height = "10px";
head.style.display = "block";

总的来说,单一的设置会导致浏览器绘制一次,而且,jstyle只是针对行类样式,对于 link 引入的样式无法获取。样式属性名的兼容问题,比如驼峰,保留字 float

样式规则

任何支持 style 特性的 HTML 元素在 js 中有一个对象的 style 属性,其实也是一个实例,但是内部属性命名都是采用的驼峰形式的,比如 background-image 要写成 backgroundImage,其中一个是比较特殊的就是 float,保留字嘛所以就换成 cssFloat,IE : styleFloat,然后对于width、hight这些处理都最好要有一个量度单位。

合并cssText

可能针对一种情况给出的处理就是 cssText 合并。

var head= document.getElementById("head");
head.style.cssText="width:20px;height:10px;display:bolck";

innerHTML 一样,cssText 很快捷且所有浏览器都支持。此外当批量操作样式时,cssText 只需一次 reflow,提高了页面渲染性能。当然如果是在创建的时候,我们还可以利用文档碎片,缺点自然就是样式被整体覆盖了,所以在处理的时候应该要先获取需要保留的样式然后再拼接起来。

样式访问

ele.style 只能获取行类样式了,那么CSS外部样式表定义的样式如何处理?

DOM2 规范增加了 defauleView 接口,提供了 getComputedStyle() 方法可以返回了类似 style 属性的 css 的属性合集。

getComputedStyle与style的区别: 区别就在于 getComputedStyle 是只能读的, style 是可以可读可写的。

jq中:

var 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"
    if ( elem.ownerDocument.defaultView.opener ) {
        return elem.ownerDocument.defaultView.getComputedStyle( elem, null );
    }

    return window.getComputedStyle( elem, null );
};

elem.ownerDocument.defaultView.getComputedStyle() 如果没有defauleView前缀,查了下在浏览器中,该属性返回当前 document 对象所关联的 window 对象,如果没有,会返回 null,应该就能直接window调用了。所以这句话就相当于window.getComputedStyle().getComputedStyle方法IE6~8是不支持的。

以上就是样式操作需要了解的基础,那么总的来说 jQuery 需要的处理问题就显而易见了。

  1. 参数传递
  2. 命名规范
  3. 访问规则
  4. 性能优化

$.css()

这是$.css()静态方法,不是jq对象方法.css().css()会处理一些其他的再调用静态方法,先看看静态方法:

// 默认computedStyle/currentStyle方式只读,也可styles指定读取对象
css: function( elem, name, extra, styles ) {
    var val, num, hooks,
        origName = jQuery.camelCase( name );

    // Make sure that we're working with the right name
    //name修正,属性兼容(同style)
    name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) );

    // Try prefixed name followed by the unprefixed name
    // 钩子
    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
    // 没有钩子,通过封装的curCSS读取
    if ( val === undefined ) {
        val = curCSS( elem, name, styles );
    }

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

    // Make numeric if forced or a qualifier was provided and val looks numeric
    // extra === "" 去单位处理,若为"normal"、"auto"等字符串,原样返回
    // extra === true 强制去单位,若为parseFloat后为NaN的字符串,返回0
    // extra === false/undefined 不特殊处理
    if ( extra === "" || extra ) {
        num = parseFloat( val );
        return extra === true || jQuery.isNumeric( num ) ? num || 0 : val;
    }
    return val;
}

首先是检测是是否驼峰写法了,如果不是就得转化下:

origName = jQuery.camelCase( name );

正则带入后:

"background-image".replace( /-([\da-z])/gi, function( all, letter ) {
    return letter.toUpperCase();
});

找到"-"后面的,转成大写开头。

修正命名 float :

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

cssProps里面其实:

cssProps: {
    "float": "cssFloat"
},

$.cssHooks 有以下钩子:

20161208154402.png

这就表示,如果是其他设置项,就会通过 curCSS 处理的取值,这是是默认的动作:

function curCSS( elem, name, computed ) {
    var width, minWidth, maxWidth, ret,
        style = elem.style;

    computed = computed || getStyles( elem );

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

    if ( computed ) {

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

        // Support: iOS < 6
        // A tribute to the "awesome hack by Dean Edwards"
        // iOS < 6 (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 ) ) {
            // 为了兼容有的浏览器margin相关方法返回百分比等非px值的情况,由于width输出是px,并且margin的百分比是按照width计算的
            // 因此可以直接赋值width。设置minWidth/maxWidth是为了保证设置的width不会因为超出限制失效

            // Remember the original values
            // 记忆
            width = style.width;
            minWidth = style.minWidth;
            maxWidth = style.maxWidth;

            // Put in the new values to get a computed value out
            // 把margin的值设置到width,并获取对应width值作为结果
            style.minWidth = style.maxWidth = style.width = ret;
            ret = computed.width;

            // Revert the changed values
            // 还原
            style.width = width;
            style.minWidth = minWidth;
            style.maxWidth = maxWidth;
        }
    }
    // 都以字符串返回
    return ret !== undefined ?
        // Support: IE
        // IE returns zIndex value as an integer.
        ret + "" :
        ret;
}

默认使用getStyles,也可通过computed参数指定样式对象。内部还有对文档片段和margin类属性值的特殊处理

$.style()

处理的方式与css类似,只是要注意了style才具有样式的修改权限

这样的传对象其实都是需要调用多次style处理的,当然没有采用cssText的方式处理,因为本身以前的属性就会丢失了

var color = a.css({"background-color":'red','width':'200px'});

.css的实现:

jQuery.fn.extend({
    css: function( name, value ) {
        return access( this, function( elem, name, value ) {
            var styles, len,
                map = {},
                i = 0;

            if ( jQuery.isArray( name ) ) {
                styles = getStyles( elem );
                len = name.length;

                for ( ; i < len; i++ ) {
                    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 );
    },
});

当时设值操作的时候,jQuery.style( elem, name, value ) 起作用:

// elem.style方式读写
style: function( elem, name, value, extra ) {

    // elem为文本和注释节点直接返回
    if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
        return;
    }

    var ret, type, hooks,
        origName = jQuery.camelCase( name ),//name修正,属性兼容
        style = elem.style;

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

    // 先看看有没有钩子 name、后origName使钩子更灵活,既可统一,又可单独
    hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];

    // Check if we're setting a value
    // elem.style方式 - 赋值
    if ( value !== undefined ) {
        type = typeof value;

        // Convert "+=" or "-=" to relative numbers (#7345)
        // '+='、'-='增量运算
        if ( type === "string" && (ret = rrelNum.exec( value )) ) {
            // adjustCSS对初始值和value进行单位换算,相加/减得到最终值(数值)
            value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) );
            // Fixes bug #9237
            // 数值需要在下面加上合适的单位
            type = "number";
        }

        // Make sure that null and NaN values aren't set (#7116)
        if ( value == null || value !== value ) {
            return;
        }

        // If a number, add 'px' to the (except for certain CSS properties)
        // 数值和'+=xx'转换的数值,都需要加上单位。cssNumber记录了可以是数字的属性,否则默认px
        if ( type === "number" && !jQuery.cssNumber[ origName ] ) {
            value += "px";
        }

        // Support: IE9-11+
        // background-* props affect original clone's values
        if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) {
            style[ name ] = "inherit";
        }

        // If a hook was provided, use that value, otherwise just set the specified value
        // 有钩子先使用钩子,看返回值是否为undefined决定是否style[ name ]赋值,否则直接赋值
        if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) {
            style[ name ] = value;
        }

    } else { // elem.style方式 - 取值
        // If a hook was provided get the non-computed value from there
        // 有钩子先使用钩子,看返回值是否为undefined决定是否style[ name ]取值,否则直接取值
        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 ];
    }
},

jQuery.style()的处理流程:

  1. 分解参数
  2. 转换为驼峰式,修正属性名
  3. 如果有钩子,则调用钩子的set get
  4. 最终实现都是依靠浏览器自己的API的

.addClass

dom就提供 elem.className 来获取class值,经过字符串拼接处理,最后elem.className = finalValue;就可以完成class的添加了,代码就不贴了。


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

赏个馒头吧