由一個「bug」到不爲人知的jQuery.cssHooks

寫在最前

本次分享一下在一次jQuery賦值樣式失效的結果中來分析背後緣由的過程。在翻jQuery源碼的過程當中,感受真是還不能說本身只是會用jQuery,我好像連會用都達不到(逃css

歡迎關注個人博客,不按期更新中——html

一個很簡單的賦值問題

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

image

我只是單純的想控制一個left值,你們都懂,可是居然失敗了,打印出的元素屬性中能夠看到left爲"";我其實一開始沒想到多是jQuery自己的緣由致使的,我先考慮的是我這個元素是否是當前要賦值的?js的問題?等等。。幹想了半天,認爲可能仍是自己的寫法問題。因此進行了以下實驗:vue

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

image

看起來是字符串和數字的區別!omg,歷來沒想過字符串和數字的效果居然會不一致。。你覺得事情已經結束了?no,看下面這個:react

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

image

好的爲何,width設定字符串就能夠被添加px後綴,left就不能夠??jquery

如今咱們能夠總結一下經過jQuery.fn.css方法來設定元素屬性的時候會有一些不一致的狀況,以width和left爲例子(由於屬性不少,不一致的狀況不少,瞭解原理便可):git

  • left經過number類型能夠補全px完成樣式設定,string類型沒法設定屬性
  • width都可以經過number或string類型完成設定屬性

從而能夠拋出由一開始的奇怪現象的底層問題:爲何經過jQuery.fn.css方法設定樣式時,string類型的值在某些屬性上沒法生效?github

從源碼中找線索

jQuery的源碼相比react、vue相比應該是很直接的了,就是一個js。(不過我仍然看不懂?web

首先引入一個沒有壓縮過的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下交流。

相關文章
相關標籤/搜索