jQuery源碼解析之Data

jQuery源碼學習之data

jQuery中有兩個關於data操做的方法html

$().data()

$.data(elem);

內部其實現均離不開自定義類Datanode

內部類 Data

Datasrc/data/Data.js定義,構建時爲實例添加expando屬性,做爲惟一標識數組

function Data() {
    this.expando = jQuery.expando + Data.uid++;
}

在原型上添加了多個方法緩存

Data.prototype = {
    cache: function(){
        ...
    },
    set: function(){
        ...
    },
    get: function(){
        ...
    },
    access: function(){
        ...
    },
    remove: function(){
        ...
    },
    hasData: function(){
        ...
    }
}

在jq內部,使用cache方法獲取緩存的數據。傳入一個參數owner,表示要獲取緩存數據的對象。判斷在owner上是否有expando屬性,若是沒有,說明這個owner是否第一次調用,須要在其初始化緩存數據對象。判斷節點的類型,若是是元素節點或者document節點或者對象時,能夠設置緩存數據。若是是元素節點或者document節點,直接使用對象字面量進行賦值,屬性名是expando,值爲空對象。若是是對象的話,使用Object.defineProperty爲其定義數據,屬性名也是expando,初始化爲{},同時屬性描述符能夠更改,不可枚舉。app

Data.prototype.cache

cache: function( owner ) {

        // Check if the owner object already has a cache
        // 獲取在owner的緩存值
        var value = owner[ this.expando ];

        // If not, create one
        if ( !value ) {
            value = {};

            // We can accept data for non-element nodes in modern browsers,
            // but we should not, see #8335.
            // Always return an empty object.
            // 判斷owener類型 是否能在其上調用data
            // 在元素節點或body或對象上能夠設置data
            // 其餘節點不設置緩存數據
            if ( acceptData( owner ) ) {

                // If it is a node unlikely to be stringify-ed or looped over
                // use plain assignment
                // 此處爲owner添加屬性 key爲Data對象的expando值 創建owner和Data對象之間的鏈接
                // owner是元素節點或body
                if ( owner.nodeType ) {
                    owner[ this.expando ] = value;

                // Otherwise secure it in a non-enumerable property
                // configurable must be true to allow the property to be
                // deleted when data is removed
                // owner是對象
                // 爲owner添加expando屬性 初始化爲{},同時屬性描述符能夠更改,不可枚舉
                } else {
                    Object.defineProperty( owner, this.expando, {
                        value: value,
                        configurable: true
                    } );
                }
            }
        }

        return value;
    }

使用set來更新緩存對象,分爲data(key,value)data(obj)兩種調用狀況,保存時要將鍵名保存爲駝峯命名法。dom

Data.prototype.set

set: function( owner, data, value ) {
        var prop,
            cache = this.cache( owner );

        // Handle: [ owner, key, value ] args
        // Always use camelCase key (gh-2257)
        if ( typeof data === "string" ) {
            cache[ jQuery.camelCase( data ) ] = value;

        // Handle: [ owner, { properties } ] args
        } else {

            // Copy the properties one-by-one to the cache object
            for ( prop in data ) {
                cache[ jQuery.camelCase( prop ) ] = data[ prop ];
            }
        }
        return cache;
    }

使用get來獲取緩存對象,調用時有data(key)data()。未指定key時直接返回整個cache對象,不然返回cache[key]。鍵名也要轉爲駝峯命名。ide

Data.prototype.get

get: function( owner, key ) {
        return key === undefined ?
            this.cache( owner ) :

            // Always use camelCase key (gh-2257)
            owner[ this.expando ] && owner[ this.expando ][ jQuery.camelCase( key ) ];
    }

對調用的方式進行區分,內部調用 setget函數

經過參數的數量和類型進行區分:oop

  • key爲空時,獲取整個cache對象
  • key類型爲stringvalue===undefined 對應獲取指定值
  • 其餘調用均爲set,在set內部進行區分

Data.prototype.access

access: function( owner, key, value ) {

        // In cases where either:
        //
        //   1. No key was specified
        //   2. A string key was specified, but no value provided
        //
        // Take the "read" path and allow the get method to determine
        // which value to return, respectively either:
        //
        //   1. The entire cache object
        //   2. The data stored at the key
        //
        if ( key === undefined ||
                ( ( key && typeof key === "string" ) && value === undefined ) ) {

            return this.get( owner, key );
        }

        // When the key is not a string, or both a key and value
        // are specified, set or extend (existing objects) with either:
        //
        //   1. An object of properties
        //   2. A key and value
        //
        this.set( owner, key, value );

        // Since the "set" path can have two possible entry points
        // return the expected data based on which path was taken[*]
        return value !== undefined ? value : key;
    }

使用remove來刪除緩存對象屬性,調用時,能夠傳入一個string,表示要刪除的鍵名,或者傳入一個保存多個鍵名的string數組。鍵名也要轉爲駝峯命名。若是不傳入出參數,則直接刪除掉在owner上的緩存數據對象。學習

Data.prototype.remove

remove: function( owner, key ) {
        var i,
            cache = owner[ this.expando ];

        if ( cache === undefined ) {
            return;
        }

        if ( key !== undefined ) {

            // Support array or space separated string of keys
            if ( Array.isArray( key ) ) {

                // If key is an array of keys...
                // We always set camelCase keys, so remove that.
                key = key.map( jQuery.camelCase );
            } else {
                key = jQuery.camelCase( key );

                // If a key with the spaces exists, use it.
                // Otherwise, create an array by matching non-whitespace
                key = key in cache ?
                    [ key ] :
                    ( key.match( rnothtmlwhite ) || [] );
            }

            i = key.length;

            while ( i-- ) {
                delete cache[ key[ i ] ];
            }
        }

        // Remove the expando if there's no more data
        if ( key === undefined || jQuery.isEmptyObject( cache ) ) {

            // Support: Chrome <=35 - 45
            // Webkit & Blink performance suffers when deleting properties
            // from DOM nodes, so set to undefined instead
            // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted)
            if ( owner.nodeType ) {
                owner[ this.expando ] = undefined;
            } else {
                delete owner[ this.expando ];
            }
        }
    }

判斷owner上是否有緩存數據。

Data.prototype.hasData

hasData: function( owner ) {
        var cache = owner[ this.expando ];
        return cache !== undefined && !jQuery.isEmptyObject( cache );
    }

jQuery方法的定義

定義後內部類data後,在/src/data.js進行拓展。在jQuery添加了hasDatadataremoveData_data_removeData等方法,在jQuery.fn上添加了dataremoveData方法。

在jQuery拓展的方法,都是對Data方法的封裝,在調用時$.data()時,並沒有對setget操做區分,在Data.prototype.access內部區分set和get。下面源碼中,dataUserdataPrivData實例,分別爲緩存數據和表示jq對象是否將元素的data屬性添加到dataUser

// $.data
jQuery.extend( {
    hasData: function( elem ) {
        return dataUser.hasData( elem ) || dataPriv.hasData( elem );
    },

    data: function( elem, name, data ) {
        return dataUser.access( elem, name, data );
    },

    removeData: function( elem, name ) {
        dataUser.remove( elem, name );
    },

    // TODO: Now that all calls to _data and _removeData have been replaced
    // with direct calls to dataPriv methods, these can be deprecated.
    _data: function( elem, name, data ) {
        return dataPriv.access( elem, name, data );
    },

    _removeData: function( elem, name ) {
        dataPriv.remove( elem, name );
    }
} );

jQuery.fn上拓展的方法只有dataremoveData。在data內,先對$().data()$().data({k:v})兩個調用狀況進行處理。若是是第一種狀況,則返回在this[0]上的緩存數據對象,若是是第一次以$().data()的方式調用,同時還會將元素上的data屬性添加dataUser中,並更新dataPriv。若是是$().data({k:v})的調用方式,則遍歷jq對象,爲每一個節點更新緩存數據。其餘調用方式如$().data(k)$().data(k,v)則調用access進行處理。此處的access並不是Data.prototype.access

removeData則遍歷jq對象,刪除在每一個節點上的緩存數據。

// $().data
jQuery.fn.extend( {
    data: function( key, value ) {
        var i, name, data,
            elem = this[ 0 ], // elem爲dom對象
            attrs = elem && elem.attributes; // 節點上的屬性

        // Gets all values
        // $().data()
        if ( key === undefined ) {
            if ( this.length ) {
                data = dataUser.get( elem );

                // elem是元素節點,且dataPriv中無hasDataAttrs時執行這個代碼塊裏的代碼
                // dataPriv上的hasDataAttrs表示elem是否有data-xxx屬性
                // 初始化dataPriv後花括號內的代碼再也不執行,即如下的if內的代碼只執行一次
                if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) {
                    i = attrs.length;
                    while ( i-- ) {

                        // Support: IE 11 only
                        // The attrs elements can be null (#14894)
                        // 將elem的data-*-*屬性以*-*轉爲駝峯命名方式的值爲鍵
                        // 在dataAttr函數內保存到dataUser中
                        // 因此用$().data能夠獲取元素的data-*屬性 但修改後dom上的屬性卻不變化
                        if ( attrs[ i ] ) {
                            name = attrs[ i ].name;
                            // name爲data-xxx
                            if ( name.indexOf( "data-" ) === 0 ) {
                                // name爲xxx
                                name = jQuery.camelCase( name.slice( 5 ) );
                                dataAttr( elem, name, data[ name ] );
                            }
                        }
                    }
                    // 同時將dataPriv的hasDataAttrs屬性設置爲真,表示已經將元素屬性節點上的data屬性保存到緩存對象中
                    dataPriv.set( elem, "hasDataAttrs", true );
                }
            }

            return data;
        }
        
        // Sets multiple values
        //  $().data(obj) 此處遍歷this,即遍歷jq對象上全部的節點,並在其設置值
        if ( typeof key === "object" ) {
            return this.each( function() {
                dataUser.set( this, key );
            } );
        }

        // 除了$().data()和$().data({k:v})的其餘狀況
        return access( this, function( value ) {
            var data;

            // The calling jQuery object (element matches) is not empty
            // (and therefore has an element appears at this[ 0 ]) and the
            // `value` parameter was not undefined. An empty jQuery object
            // will result in `undefined` for elem = this[ 0 ] which will
            // throw an exception if an attempt to read a data cache is made.
            // value undefined說明是獲取操做
            // 調用$().data(k)
            if ( elem && value === undefined ) {

                // Attempt to get data from the cache
                // The key will always be camelCased in Data
                // 從dataUser中獲取 非undefined時返回
                data = dataUser.get( elem, key );
                if ( data !== undefined ) {
                    return data;
                }

                // Attempt to "discover" the data in
                // HTML5 custom data-* attrs
                // dataUser中不存在key,調用dataAttr查找元素的data-*屬性
                // 若是存在屬性,更新dataUser並返回其值
                data = dataAttr( elem, key );
                if ( data !== undefined ) {
                    return data;
                }

                // We tried really hard, but the data doesn't exist.
                return;
            }

            // Set the data...
            // jq對象長度 >= 1, 調用如$().data(k,v) 遍歷jq對象,爲每一個節點設置緩存數據
            this.each( function() {

                // We always store the camelCased key
                dataUser.set( this, key, value );
            } );
        }, null, value, arguments.length > 1, null, true );
    },

    // 遍歷jq對象,刪除各個元素上的緩存數據
    removeData: function( key ) {
        return this.each( function() {
            dataUser.remove( this, key );
        } );
    }
} );

其中,getData用於對元素上的data屬性進行類型轉換,dataAttr用於獲取保存在元素節點上的data屬性,並同時更新dataUser。須要注意的是,以$().data(k, v)方式調用時,若是在緩存數據上查找不到屬性,則會調用dataAttr在元素查找屬性。

// 屬性值是string 進行類型轉換
function getData( data ) {
    if ( data === "true" ) {
        return true;
    }

    if ( data === "false" ) {
        return false;
    }

    if ( data === "null" ) {
        return null;
    }

    // Only convert to a number if it doesn't change the string
    // data轉化成number再轉成string後仍嚴格等於data
    if ( data === +data + "" ) {
        return +data;
    }

    if ( rbrace.test( data ) ) {
        return JSON.parse( data );
    }

    return data;
}

// 獲取元素的dataset中的屬性,並保存到dataUser中
function dataAttr( elem, key, data ) {
    var name;

    // If nothing was found internally, try to fetch any
    // data from the HTML5 data-* attribute
    // 此處獲取dataset裏的值
    if ( data === undefined && elem.nodeType === 1 ) {
        name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); // 此處將駝峯命名方式的key轉化爲data-*-*
        data = elem.getAttribute( name );

        if ( typeof data === "string" ) {
            try {
                data = getData( data );
            } catch ( e ) {}

            // Make sure we set the data so it isn't changed later
            // 將元素的data-*屬性保存到dataUser中
            dataUser.set( elem, key, data );
        } else {
            data = undefined;
        }
    }
    return data;
}
相關文章
相關標籤/搜索