由一个“bug”到鲜为人知的jQuery.cssHooks

908 阅读3分钟

写在最前

本次分享一下在一次jQuery赋值样式失效的结果中来分析背后原因的过程。在翻jQuery源码的过程中,感觉真是还不能说自己只是会用jQuery,我好像连会用都达不到(逃

欢迎关注我的博客,不定期更新中——

一个很简单的赋值问题

$('#' + id).css({"left": "200"})

image

我只是单纯的想控制一个left值,大家都懂,但是竟然失败了,打印出的元素属性中可以看到left为"";我其实一开始没想到可能是jQuery本身的原因导致的,我先考虑的是我这个元素是不是当前要赋值的?js的问题?等等。。干想了半天,认为可能还是本身的写法问题。所以进行了如下实验:

$('#' + id).css({"left": 200})

image

看起来是字符串和数字的区别!omg,从来没想过字符串和数字的效果竟然会不一致。。你以为事情已经结束了?no,看下面这个:

$('#' + id).css({"width": "200"})

image

好的为什么,width设定字符串就可以被添加px后缀,left就不可以??

现在我们可以总结一下通过jQuery.fn.css方法来设定元素属性的时候会有一些不一致的情况,以width和left为例子(因为属性很多,不一致的情况很多,了解原理即可):

  • left通过number类型可以补全px完成样式设定,string类型无法设定属性
  • width均可以通过number或string类型完成设定属性

从而可以抛出由一开始的奇怪现象的底层问题:为什么通过jQuery.fn.css方法设定样式时,string类型的值在某些属性上无法生效?

从源码中找线索

jQuery的源码相比react、vue相比应该是很直接的了,就是一个js。(不过我仍然看不懂?

首先引入一个没有压缩过的jQuery,里面保留了所有的注释和代码结构,很方便大家阅读

https://cdn.bootcss.com/jquery/3.3.1/jquery.js

先找到我们本次设定样式的方法jQuery.fn.css:

jQuery.fn.extend( {
        css: function( name, value ) {
            return access( this, function( elem, name, value ) {
                var styles, len,
                    map = {},
                    i = 0;
                if ( Array.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 );
        }
    } );

如何通过浏览器来调试源码呢?(因为直接看源码太繁琐了,通过debug的形式可以看到每次的调用栈)我们可以通过console.log的形式,在这段源码中将console写入,之后在控制台中就可以看到对应源码的调用:

wechatimg152

进入jQuery.style之后就会来到最终产生区别的地方:

style: function( elem, name, value, extra ) {
    
            ...
            hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
            if ( value !== undefined ) {
                type = typeof value;
                if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {
                    value = adjustCSS( elem, name, ret );
                    type = "number";
                }
                ...
                if ( type === "number" ) {
                    value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" );
                }
                ...
                if ( !hooks || !( "set" in hooks ) ||( value = hooks.set( elem, value, extra ) ) !== undefined ) {
                    //此时的value到底是200还是200px;只有添加了后缀才能赋值成功
                    if ( isCustomProp ) {
                        style.setProperty( name, value );
                    } else {
                        style[ name ] = value;
                    }
                }
    
            } 
            ...
        },

源码中可以看到在传入的value中确实对string和number做了区分;而不是我之前所认为的,string应该和number差不多:)如果传入number类型,便会为其添加px后缀;但是这仍然没有解释为什么left和width均传入string而结果不同的问题。重点在于这句话:

hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
...
if ( !hooks || !( "set" in hooks ) ||
    ( value = hooks.set( elem, value, extra ) ) !== undefined ) {
    ...
}

在value是string类型,到最终赋值之前,还会经过value = hooks.set( elem, value, extra ) ) !== undefined的判断,也就是说如果hooks.set方法存在,我们还有一次通过这个方法来将string类型的value进行后缀补全的机会。而这个hooks是由jQuery.cssHooks得到的,那么jQuery.cssHooks是什么:

wechatimg153

从源码中可以看出,cssHooks中包含了属性的一些方法,其中left只有get;width有get和set。再结合上面的判断条件就可以推断出,由于width存在了set方法,在其方法中对string类型的value完成了后缀的补齐,而left则不行从而形成了文中一开始的“神奇”现象。

cssHooks

直接向 jQuery 中添加钩子,用于覆盖设置或获取特定 CSS 属性时的方法,目的是为了标准化 CSS 属性名或创建自定义属性。 $.cssHooks 对象提供了一种通过定义函数来获取或设置特定 CSS 值的方法。可以用它来创建新的 cssHooks 用于标准化 CSS3 功能,例如,盒子阴影(box shadows)及渐变(gradients)。

例如,某些基于 Webkit 的浏览器会使用 -webkit-border-radius 来设置对象的 border-radius,然而,早先版本的 Firefox 则使用 -moz-border-radius。cssHook 就可以将这些不同的写法进行标准化,从而让 .css() 可以使用统一的标准化属性名(border-radius 或对应的 DOM 属性写法 borderRadius)。

该方法除了提供了对特定样式的处理可以采用更加细致的控制外,$.cssHooks 同时还扩展了 .animate() 方法上的属性集。

简单来说,jQuery给我们暴露了一个钩子,我们可以自己定义方法比如set,来实现针对某个属性的特定行为。所以出现left和width的问题就是有没有set这个钩子方法。so。。我们还剩最后一个问题:

为什么width要对其设定钩子函数?

答案可以从其set方法来窥探一下:

set: function( elem, value, extra ) {
    var matches,
        styles = getStyles( elem ),
        isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
        subtract = extra && boxModelAdjustment(
            elem,
            dimension,
            extra,
            isBorderBox,
            styles
        );

    // Account for unreliable border-box dimensions by comparing offset* to computed and
    // faking a content-box to get border and padding (gh-3699)
    if ( isBorderBox && support.scrollboxSize() === styles.position ) {
        subtract -= Math.ceil(
            elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -
            parseFloat( styles[ dimension ] ) -
            boxModelAdjustment( elem, dimension, "border", false, styles ) -
            0.5
        );
    }

    // Convert to pixels if value adjustment is needed
    if ( subtract && ( matches = rcssNum.exec( value ) ) &&
        ( matches[ 3 ] || "px" ) !== "px" ) {

        elem.style[ dimension ] = value;
        value = jQuery.css( elem, dimension );
    }

    return setPositiveNumber( elem, value, subtract );
}

从这个钩子函数中我们可以看出,要对width做特殊处理是因为css的盒模型有好几种,content-box|border-box|inherit分别代表“不包括padding、border、margin” | “包含border和padding” | “继承”;故为了统一外界的调用,隐藏这些背后的判断,从而增加了这个set方法。顺带着在其中把px补全了。同时left这种没什么需要兼容的故没有设定set方法。

小结

虽然cssHooks不常用(我反正从来没用过,现在对于标准化格式有很多其他的方法来做,cssHooks的钩子感觉还是有些复杂了),但这次通过页面上一个很小的问题从而引发思考并且试图深挖一些的过程还是值得总结下来的。虽然我们不是造轮子的人,但理解别人的轮子也是比“会用”好一些的;更何况看了cssHooks我感觉我都不会用jQuery:)

参考文章

最后

惯例po作者的博客,不定时更新中——

有问题欢迎在issues下交流。