1. 程式人生 > >jQuery原始碼解析(4)—— css樣式、定位屬性

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.$
            
           

相關推薦

jQuery原始碼解析4—— css樣式定位屬性

閒話 原計劃是沒有這篇博文的,研究animation原始碼的時候遇到了css樣式這個攔路虎。比如jQuery支援“+=10”、“+=10px”定義一個屬性的增量,但是有的屬性設定時可以支援數字,有的必須有單位;在對屬性當前值讀取時,不同的瀏覽器可能返回不同的單

jQuery原始碼解析1—— jq基礎data快取系統

閒話 jquery 的原始碼已經到了1.12.0 版本,據官網說1版本和2版本若無意外將不再更新,3版本將做一個架構上大的調整。但估計能相容IE6-8的,也許這已經是最後的樣子了。 我學習jq的時間很短,應該在1月,那時的版本還是1.11.3,通過看妙味課堂

jQuery原始碼解析2—— CallbackDeferred非同步程式設計

閒話 這篇文章,一個月前就該出爐了。跳票的原因,是因為好奇標準的promise/A+規範,於是學習了es6的promise,由於興趣,又完整的學習了《ECMAScript 6入門》。 本文目的在於解析jQuery對的promise實現(即Deferred,是

Spring原始碼解析4:IOC過程下

上文說到populateBean方法中,對被@Autowired註解的屬性方法進行注入。在這之後,BeanFactory執行applyPropertyValues方法,這個方法中,一個是把之前解析出來的屬性值設定到bean中去;一個是繼續解析出BeanDefinition中定

死磕 java同步系列之ReentrantLock原始碼解析——公平鎖非公平鎖

問題 (1)重入鎖是什麼? (2)ReentrantLock如何實現重入鎖? (3)ReentrantLock為什麼預設是非公平模式? (4)ReentrantLock除了可重入還有哪些特性? 簡介 Reentrant = Re + entrant,Re是重複、又、再的意思,entrant是enter的名詞或

Spring原始碼解析——元件註冊4

  /** * 給容器中註冊元件; * 1)、包掃描+元件標註註解(@Controller/@Service/@Repository/@Component)[自己寫的類] * 2)、@Bean[匯入的第三方包裡面的元件] * 3)、@Import[快速給容器中匯入一個

jQuery原始碼解析jQuery物件的例項屬性和方法

1、記錄版本號 以及 修正constructor指向 jquery: core_version, constructor: jQuery,   因為jQuery.prototype={ ... } 這種寫法將自動生成的jQuery.prototype.constructor

jquery 1.7.2原始碼解析構造jquery物件

構造jquery物件 jQuery物件是一個類陣列物件。 一)建構函式jQuery() 建構函式的7種用法:   1.jQuery(selector [, context ]) 傳入字串引數:檢查該字串是選擇器表示式還是HTML程式碼。如果是選擇器表示式,則遍歷文件查詢匹配的DOM元

jQuery深入之原始碼解析

總體架構 可以看出來jQuery主要有三個模組: 入口模組、功能模組、底層支援模組。 - 入口模組 在構造jQuery物件模組中,如果在呼叫建構函式建立jQuery物件時,會呼叫選擇器

jQuery原始碼解析架構與依賴模組

jQuery有3種針對文件載入的方法$(document).ready(function() { // ...程式碼... }) //document ready 簡寫 $(function() { // ...程式碼... }) $(document).load(function() {

HLS學習HLSDownloader原始碼分析4解析Master PlayList

解析Master PlayList     PlayList就是m3u8檔案或者索引檔案,Master PlayList也叫一級索引檔案。 解析Master PlayList的過程如下: 1、

jQuery源代碼解析1—— jq基礎data緩存系統

代碼解析 post 方法 step 作用域鏈 垃圾清理 版本 get initial 閑話 jquery 的源代碼已經到了1.12.0 版本號。據官網說1版本號和2版本號若無意外將不再更新,3版本號將做一個架構上大的調整。但預計能兼容IE6-8的。或許

jQuery源代碼解析3—— ready載入queue隊列

else ng- settime eve ref promise ont 出隊 function ready、queue放在一塊寫,沒有特殊的意思,僅僅是相對來說它倆可能源代碼是最簡單的了。ready是在dom載入完畢後。以最高速度觸發,非常實用。que

Mybatis原始碼分析4—— Mapper的建立和獲取

Mybatis我們一般都是和Spring一起使用的,它們是怎麼融合到一起的,又各自發揮了什麼作用? 就拿這個Mapper來說,我們定義了一個介面,聲明瞭一個方法,然後對應的xml寫了這個sql語句, 它怎麼就執行成功了?這傢伙是怎麼實現的,帶著這個好奇心,我一步步跟蹤,慢慢揭開了它的

HashMap原始碼解析JDK8

前言 這段時間有空,專門填補了下基礎,把常用的ArrayList、LinkedList、HashMap、LinkedHashMap、LruCache原始碼看了一遍,List相對比較簡單就不單獨介紹了,Map準備用兩篇的篇幅,分別介紹HashMap和(LruCache+LinkedHa

Spring原始碼解析十三——AOP原理——AnnotationAwareAspectJAutoProxyCreator註冊

 * 2、 AnnotationAwareAspectJAutoProxyCreator:  *         AnnotationAwareAspectJAutoProxyCreator &nbs

Spring原始碼解析——生命週期——BeanPostProcessor在spring底層的使用

一、ApplicationContextAwareProcessor import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import or

Spring原始碼解析——生命週期——BeanPostProcessor

https://blog.csdn.net/u011734144/article/details/72600932 http://www.cnblogs.com/lucas2/p/9430169.html   BeanPostProcessor:bean的後置處理器。在bean

Spring原始碼解析——元件註冊3

@Scope設定元件作用域 import com.ken.domain.Person; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Config

Spring原始碼解析——元件註冊2

    import com.ken.service.BookService; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.