1. 程式人生 > >jQuery原始碼解析(1)—— jq基礎、data快取系統

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

閒話

jquery 的原始碼已經到了1.12.0 版本,據官網說1版本和2版本若無意外將不再更新,3版本將做一個架構上大的調整。但估計能相容IE6-8的,也許這已經是最後的樣子了。

我學習jq的時間很短,應該在1月,那時的版本還是1.11.3,通過看妙味課堂的公開課視訊和文件裡的所有api的註解學習。

原始碼則是最近些日子直接生啃,跳過了sizzle和文件處理的部分(待業狗壓力大,工作以後再看),關注datareadyeventqueueDefferred(jq的promise程式設計)、ajaxanimation的處理,初看甚至有點噁心,耐著性子堅持卻嘆其精妙,在這裡記錄下來加深印象。

(本文采用 1.12.0 版本進行講解,用 #number 來標註行號)

jQuery初始化

整體封裝上,考慮了兩點(寫輪子的重要參考)

模組化支援:通過 noGlobal 變數來決定是否繫結到全域性變數,返回值為jQuery
衝突避免:儲存window.$ window.jQuery,可以呼叫noConflict 還原,返回jQuery物件。(noGlobal為true時不需要)

主體使用瞭如下結構,抽離 if 分支,只關注主體邏輯的書寫

(function( a, fun ) {
    // 判斷是否呼叫、如何呼叫fun
})(a, function( _a, _b )
{
// 具體邏輯 }); /* ---- 區別於如下模式 ----*/ (function( _a, _b) { if (判斷A) { // 調整引數或退出 } else if (判斷B) { // 調整引數或退出 } ... // 這裡開始時具體邏輯 })( a );

[原始碼]

(function( global, factory ) {

    if ( typeof module === "object" && typeof module.exports === "object" ) {
        module.exports = global.document ?
            factory( global, true
) : // 若無window,則模組存為函式,可取出通過傳入window來呼叫一次 // noGlobal為false,一定會汙染全域性 function( w ) { if ( !w.document ) { throw new Error( "jQuery requires a window with a document" ); } return factory( w ); }; } else { factory( global ); } })(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { /* ---- jQuery具體邏輯 ----*/ ... // 對AMD的支援,#10991 if ( typeof define === "function" && define.amd ) { define( "jquery", [], function() { return jQuery; } ); } // 儲存之前的 window.$ window.jQuery var _jQuery = window.jQuery, _$ = window.$; jQuery.noConflict = function() { if ( window.$ === jQuery ) { window.$ = _$; } if ( deep && window.jQuery === jQuery ) { window.jQuery = _jQuery; } return jQuery; }; // 模組化時,不設定全域性 if ( !noGlobal ) { window.jQuery = window.$ = jQuery; } return jQuery; });

jQuery工廠結構

jq為了能有$.func、 $().func兩種呼叫方法,選擇了共享 jQuery.prototypereturn new jQuery.fn.init

這並不是唯一的方式(可以如下),之所以選擇如此,個人認為應該是使用頻率太高,這樣每次可以省掉兩次型別判斷。而 jQuery.fn 我想也是起到簡寫、別名的作用

jQuery.extend/fn.extend 則決定了jq重要的外掛擴充套件機制

var jQuery = function( selector, context ) {
    if ( this instanceof jQuery) {
        return new jQuery( selector, context );     
    }
    // 具體邏輯
}

[原始碼]

// #71
var jQuery = function( selector, context ) {
    return new jQuery.fn.init( selector, context );
};
// #91, 原型的別名 fn,省字元
jQuery.fn = jQuery.prototype = {
    ...
};
// #175, jQuery 擴充套件方法extend定義,只填一個引數表示對this進行擴充套件
jQuery.extend = jQuery.fn.extend = function(){
    ...
};
// #2866, 支援選擇器,節點html,element,jq物件,函式
var init = jQuery.init = function( selector, context, root ) {
    ...
};
// #2982, 跟jQuery共享原型物件
init.prototype = jQuery.fn;

jQuery鏈式呼叫

精髓:通過 return this , return this.pushStack() , return this.prevObject 實現鏈式呼叫、增棧、回溯

[原始碼]

// # 122, 建立一層新的堆疊, 並引用 prevObject
pushStack: function( elems ) {
    // merge -> #433, 支援把陣列、類陣列的0-length項新增到第一個引數
    var ret = jQuery.merge( this.constructor(), elems );

    ret.prevObject = this;
    ret.context = this.context;

    return ret;
};

// #164, 可以回溯上一級 preObject
end: function() {
    return this.preObject || this.constructor();
}

// #3063, 新增到新層 this.constructor()
add: function( selector, context ) {
    return this.pushStack(
        // 去重排序
        jQuery.uniqueSort(
            // 合併到 this.get()
            jQuery.merge( this.get(), jQuery( selector, context ) )
        )
    );
},
addBack: function( selector ) {
    // add將把結果pushStack
    return this.add( selector == null ?
        this.preObject : 
        // 可做一次過濾
        this.prevObject.filter( selector )
    );
}

看到這,說明你真的是個有耐心的boy了。我們上主菜!

IE記憶體洩露

講data快取前,首先要必須介紹一下IE6-8的記憶體洩露問題。IE低版本dom採用COM元件的形式編寫,垃圾清理機制,使用的引用計數的方式,無法正確的處理包含dom元素的環狀的迴圈引用。即使重新整理頁面記憶體也不會被釋放。

通常是如何產生迴圈引用的呢?

此例中,當前作用域裡包含變數a (引用了dom元素),通過繫結事件onclick,使dom引用了事件函式。但是函式在宣告時,會把當前所在活動物件的作用域鏈儲存在自身的scope屬性,從而引用了當前環境定義的所有變數,而變數a指向dom,從而產生迴圈引用。需要手動把變數對dom的引用解除

{ // 某個scope作用域
    var a = document.getElementById('id_a');
    // dom -> fun
    a.onclick = function(e) {
    };
    // fun -> a -> dom , 解除對dom元素的引用, fun -> a -X- dom
    a = null;
}

jQuery.data原理

jq內部實現了一個快取機制,用於分離dom對函式等物件的引用(函式對dom的引用取決於函式本身的特性、定義的位置,這個沒法子)。如何能做到呢?與 策略模式 的思路一致——字串約定。

jQuery.extend({
    // #247, 構造一個基本不可能被佔用的屬性, 除去"."
    expando: "jQuery" + ( version + Math.random() ).replace(/\D/g, ""),
    // #507, 全域性使用的 guid 號碼
    guid: 1,
    // #4014, 快取空間
    cache: {}
});

/* ---- 原理示意 ---- */
var a = document.getElementById("id_a");

// 每個元素的 jQuery.expando 分到唯一的 guid 號碼
a[jQuery.expando] = a[jQuery.expando] || jQuery.guid++;

// jQuery.cache 上元素的 guid 對應的位置為該物件的快取空間
jQuery.cache[a[jQuery.expando]] = {};

通過 jQuery.data 的封裝可以輕鬆實現資料的存取,但是這樣就結束了麼?太天真了!

這樣做也會產生衍生的問題。雖然不產生迴圈引用,但物件繫結在jQuery.cache上,即使dom物件被移除,資料仍然不會因此消失。而且如果是函式,仍然會通過引用環境的變數單向引用著dom,導致dom元素也不消失。雖然重新整理後記憶體會清出,不如迴圈引用嚴重,但也是問題了。

看起來仍然需要手動設定變數為null,彷彿回到原點,但是資料還在,比之前更不如。解決方法其實很簡單,就是移除節點時呼叫 jQuery.cleanData ,data沒有被任何物件引用,自然的回收。

但是問題仍然沒解決,因為例如繫結事件,即使函式放在jQuery.cache中,也至少有一個觸發函式繫結在dom上,因此 jQuery.event.add( elem, types, handler, data, selector ) 中的elem返回前被設為null ,見原始碼 #4961。所以如非事事通明,儘量使用jq提供的方式刪除節點、繫結事件等

function remove( elem, selector, keepData ) { // #6107, #6255為正式方法
    var node,
        elems = selector ? jQuery.filter( selector, elem ) : elem,
        i = 0;

    for ( ; ( node = elems[ i ] ) != null; i++ ) {

        if ( !keepData && node.nodeType === 1 ) {
            // 清除資料
            jQuery.cleanData( getAll( node ) );
        }

        if ( node.parentNode ) {
            if ( keepData && jQuery.contains( node.ownerDocument, node ) ) {
                setGlobalEval( getAll( node, "script" ) );
            }
            // 刪除節點
            node.parentNode.removeChild( node );
        }
    }

    return elem;
}

jQuery架構方法

jQuery在此之上考慮了幾點:

核心

  1. 對普通物件和dom物件做了區分。因為普通物件的垃圾回收機制(GC)使用的不是引用計數,不會記憶體洩露。其次data機制本身增加了複雜度不說,物件解除引用後繫結的資料還需要手動銷燬,反而造成記憶體的無端佔用。

    普通物件繫結在自身的 jQuery.expando 屬性上,初始化為 { toJSON: jQuery.noop },防止序列化時暴露資料。dom物件繫結在 jQuery.cache[ ele[jQuery.expando] ] ,初始化為 {}

  2. 私用和公用隔離,如 jQuery.cache[ guid ]、jQuery.cache[ guid ].data。內部的事件、佇列等等都使用的私有空間呼叫 _data 方法,而使用者儲存資料呼叫的 data 方法則是私有,由引數 pvt 決定,true代表私有

  3. 當移除資料後,jQuery.cache[ guid ]或 jQuery.cache[ guid ].data物件不再有資料,則移除該物件釋放記憶體。且當移除快取物件時,繫結在elem的事件函式也將被移除

特性

  1. 支援通過簡單的自定義配置來增加不支援快取的特例型別

  2. 擴充套件支援讀取 elem.attributes 中 data- 字首定義的資料

  3. 以小駝峰書寫為標準讀取方式,內部進行相應轉換

jQuery.data結構

使用常見的外觀模式,定義核心功能,通過再封裝實現個性化的使用規則,以供其他模組或外部呼叫

  • 核心功能:(特點:引數多而全,邏輯負責全面)

    jQuery.internalData( elem, name, data, pvt ) 為dom和物件設定data資料,支援 -> 核心12
    jQuery.internalRemoveData( elem, name, pvt ) 移除dom或物件上指定屬性的data資料-> 核心3
    jQuery.cleanData( elems, forceAcceptData ) 由於刪除data時還要刪除elem本身上繫結的觸發事件函式,因此不能簡單 delete cache[id]。
    單獨封裝,被事件系統和 jQuery.internalRemoveData使用 ->核心3
    
  • 外觀:(工具方法、例項方法)

    jQuery.hasData( elem ) 直接在對應的 `cache` 中查詢。
    jQuery._data( elem, name, data )  jQuery.\_removeData( elem, name )`用於內部私有呼叫,封裝了核心方法,簡化了傳引數量。
    jQuery.data( elem, name, data )  jQuery.removeData( elem, name ) 用於外部公共呼叫,封裝了核心方法,簡化了傳引數量。
    
    jQuery.fn.data( key, value )  jQuery.fn.removeData( key, value ) 封裝了 jQuery.data  jQuery.removeData ,遍歷this來分別呼叫
    
  • 鉤子:埋在某個環節直接執行,根據需要實現終止流程、改變特性的功能,或不產生任何影響

    acceptData( elem ) #3762,特性1,過濾
    dataAttr( elem, key, data ) #3780,特性2,data的值不存在則會訪問元素的attribute,返回data,且值會快取到cache
    jQuery.camelCase( string ) #356,特性3,小駝峰
    

[核心 + 外觀 + 鉤子] 感覺應該算一種常見好用的架構方式了。jq中用到了很多 jQuery.some() -> 核心, jQuery.fn.some() -> 外觀 的形式,也都差不多。

這裡提一下我所理解的鉤子,Callback的四個字串特性就類似於四個鉤子,在最常見的幾個需求分歧上埋設,發展了觀察者模式的4種特性支援,event的眾多鉤子 標準方式轉換 + 個例bug修正 完成了相容的大業與主邏輯的統一。

另外不得不提一個在此維度之外的優化方案 —— 提煉重複程式碼。核心邏輯是不可省的。通常支援數種方式定義引數,把其變化為某種固定形式的引數傳入來執行遞迴是分離函式內部個性化與核心邏輯的第一步,還可以進一步,抽出此部分邏輯,保持主邏輯的純粹,變成外觀部分內的邏輯進行 功能增強。jq再進了一步,由於大量方法都支援引數判斷存取、物件或字串均可、map對映,分離出 access( elems, fn, key, value, chainable, emptyGet, raw ) #4376,核心 jQuery.xxx(),外觀jQuery.fn.xxx() 裡呼叫 access`。

// access( elems, fn, key, value, chainable, emptyGet, raw )
// 直觀語義是讓forEach( elems, fn( elem, key, value ) ), 支援型別判斷實現對映等功能

css: function( name, value ) {  // #7340, 隨意舉的例子

        // 第二個引數要麼是 jQuery.someThing
        // 或者是封裝了 jQuery.someThing.apply( ... ) 的函式,內含
        return access( this, function( elem, name, value ) {
            var styles, len,
                map = {},
                i = 0;

            // 新增一樣只針對css的個性化引數處理
            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 );
    }

但是,data快取系統並沒有這樣使用。原因也很簡單,基礎的共性部分支援不同不支援對映,如上面的css是共性相同的情況下可以增加個性,但不同的情況就要重新抽出、或寫在外觀裡、或寫在核心程式碼裡使用遞迴。



[原始碼]

/* ---------------------------------- 1. 相關程式碼(掃一下) ---------------------------------- */

jQuery.extend({
    // #247, 構造一個基本不可能被佔用的屬性, 除去"."
    expando: "jQuery" + ( version + Math.random() ).replace(/\D/g, ""),
    // #507, 全域性使用的 guid 號碼
    guid: 1,
    // #4014, 快取空間
    cache: {},
    noData: {
        // 為 true 的不可以
        "applet ": true,
        "embed ": true,

        // ...but Flash objects (which have this classid) *can* handle expandos
        "object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
    },
});

// #3748, support.deleteExpando
( function() {
    var div = document.createElement( "div" );

    // Support: IE<9
    support.deleteExpando = true;
    try {
        delete div.test;
    } catch ( e ) {
        support.deleteExpando = false;
    }

    // Null elements to avoid leaks in IE.
    div = null;
} )();

// #284, 可用於檢測 elem 的公有 cache.data 是否已空
jQuery.isEmptyObject = function( obj ) {
    var name;
    for ( name in obj ) {
        return false;
    }
    return true;
};

// #3814, 檢測 elem 的私有 cache 快取是否已空
function isEmptyDataObject( obj ) {
    var name;
    for ( name in obj ) {

        // if the public data object is empty, the private is still empty
        if ( name === "data" && jQuery.isEmptyObject( obj[ name ] ) ) {
            continue;
        }
        if ( name !== "toJSON" ) {
            return false;
        }
    }

    return true;
}

/* ---------------------------------- 2. 鉤子(瞅瞅) ---------------------------------- */

// #3762
var acceptData = function( elem ) {

    // 比對禁止列表 jQuery.noData, 為 true 必然不能
    var noData = jQuery.noData[ ( elem.nodeName + " " ).toLowerCase() ],
        // 普通物件都會按 nodeType = 1 處理,通過篩選
        nodeType = +elem.nodeType || 1;

    return nodeType !== 1 && nodeType !== 9 ?
        false :

        // noData不存在(黑名單) 或 存在且classid與noDta相同但不為true(白名單)
        !noData || noData !== true && elem.getAttribute( "classid" ) === noData;
};

function dataAttr( elem, key, data ) {

    // 正確姿勢: dataAttr( elem, key, jQuery.data( elem )[ key ] );
    // data 不存在,則查詢 HTML5 data-* attribute,並存入 jQuery.data( elem, key, data )
    // 返回 data
    if ( data === undefined && elem.nodeType === 1 ) {

        var name = "data-" + key.replace( /([A-Z])/g, "-$1" ).toLowerCase();

        data = elem.getAttribute( name );

        if ( typeof data === "string" ) {
            try {
                // "true" -> true , "false" -> false , "23" -> 23
                // "{ \"a\": 1}" -> {"a": 1}, "[1, '2']" -> [1, '2']
                // 或 data 本身
                data = data === "true" ? true :
                    data === "false" ? false :
                    data === "null" ? null :
                    +data + "" === data ? +data :
                    /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/.test( data ) ? jQuery.parseJSON( data ) :
                    data;
            } catch ( e ) {}

            // 儲存
            jQuery.data( elem, key, data );

        } else {
            data = undefined;
        }
    }

    return data;
}

// #356, 正則變數替換了,本身在 #80
jQuery.camelCase = function( string ) {
    // -ms-abc -> msAbc , a-abc -> aAbc
    return string.replace( /^-ms-/, "ms-" ).replace( /-([\da-z])/gi, fcamelCase );
};


/* ---------------------------------- 3. 核心(關鍵) ---------------------------------- */

// #3830, 新增快取
function internalData( elem, name, data, pvt /* true 為私,存cache;false 為公,存cache.data */ ) {
    // 鉤子,黑白名單
    if ( !acceptData( elem ) ) {
        return;
    }

    var ret, thisCache,
        internalKey = jQuery.expando,

        // dom 和 object 區分對待
        isNode = elem.nodeType,

        // object 不快取,存自身 jQuery.expando 屬性上
        cache = isNode ? jQuery.cache : elem,

        // 沒設定過說明 第一次
        id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;

    // 1. 第一次只能寫,不能讀。否則 return
    if ( ( !id || !cache[ id ] || ( !pvt && !cache[ id ].data ) ) &&
        data === undefined && typeof name === "string" ) {
        return;
    }

    // 2. 第一次寫,先初始化 [ 屬性、快取物件 cache ]
    // dom -> jQuery.cache[ elem[ internalKey ] ],object -> object[internalKey]
    if ( !id ) {

        // Only DOM nodes need a new unique ID for each element since their data
        // ends up in the global cache
        if ( isNode ) {
            id = elem[ internalKey ] = deletedIds.pop() || jQuery.guid++;
        } else {
            id = internalKey;
        }
    }

    if ( !cache[ id ] ) {
        cache[ id ] = isNode ? {} : { toJSON: jQuery.noop };
    }

    // 3. 寫入與讀取
    // write特例:支援 name 引數為 { "key" : value } 或 函式。存入資料
    if ( typeof name === "object" || typeof name === "function" ) {
        // 公私不同
        if ( pvt ) {
            cache[ id ] = jQuery.extend( cache[ id ], name );
        } else {
            cache[ id ].data = jQuery.extend( cache[ id ].data, name );
        }
    }

    thisCache = cache[ id ];

    // thisCache 根據pvt索引到了應該被賦值的物件
    if ( !pvt ) {
        if ( !thisCache.data ) {
            thisCache.data = {};
        }

        thisCache = thisCache.data;
    }

    // 寫入,小駝峰為標準
    if ( data !== undefined ) {
        thisCache[ jQuery.camelCase( name ) ] = data;
    }

    // Check for both converted-to-camel and non-converted data property names
    // If a data property was specified
    if ( typeof name === "string" ) {

        // 這不是重點,也可以讀非駝峰。正常使用並不會有
        ret = thisCache[ name ];

        if ( ret == null ) {

            // 讀這裡,無論讀寫,都要讀一次
            ret = thisCache[ jQuery.camelCase( name ) ];
        }
    } else {
        // 無 name , 直接讀 cache
        ret = thisCache;
    }

    return ret;
}


// #3922, 刪除 公/私 快取屬性 或 快取。
// 刪完屬性若變空cache,將移去。會處理掉 elem 上 繫結的 event
function internalRemoveData( elem, name, pvt ) {

    // 鉤子,黑名單者,直接 return
    if ( !acceptData( elem ) ) {
        return;
    }

    var thisCache, i,
        isNode = elem.nodeType,
        cache = isNode ? jQuery.cache : elem,
        id = isNode ? elem[ jQuery.expando ] : jQuery.expando;

    // 快取空間未初始化,return
    if ( !cache[ id ] ) {
        return;
    }

    // 1. name 存在,刪除 name 屬性值
    if ( name ) {

        thisCache = pvt ? cache[ id ] : cache[ id ].data;

        if ( thisCache ) {

            // 1.1 支援陣列定義多屬性,此處把字串形式也轉為陣列[name]
            // next step: 統一迭代刪除
            if ( !jQuery.isArray( name ) ) {

                // 這不是重點,也可以讀非駝峰。正常使用並不會有
                if ( name in thisCache ) {
                    name = [ name ];
                } else {

                    // 看這裡,轉換為小駝峰讀
                    name = jQuery.camelCase( name );
                    if ( name in thisCache ) {
                        name = [ name ];
                    } else {
                        // 可以字串空格隔開多個,均變成小駝峰
                        name = name.split( " " );
                    }
                }
            } else {

                // If "name" is an array of keys...
                // When data is initially created, via ("key", "val") signature,
                // keys will be converted to camelCase.
                // Since there is no way to tell _how_ a key was added, remove
                // both plain key and camelCase key. #12786
                // This will only penalize the array argument path.
                name = name.concat( jQuery.map( name, jQuery.camelCase ) );
            }

            // 1.2 刪
            i = name.length;
            while ( i-- ) {
                delete thisCache[ name[ i ] ];
            }

            // 1.3 如果 cache 刪除後沒空,結束 return
            // 如果空了, 與 name 不存在的情況一樣直接刪除 data
            if ( pvt ? !isEmptyDataObject( thisCache ) : !jQuery.isEmptyObject( thisCache ) ) {
                return;
            }
        }
    }

    // 2. 根據 pvt 判斷,false 刪除公有
    if ( !pvt ) {
        delete cache[ id ].data;

        // cache 還沒空,可以閃了,return
        // cache 空了,合併到 pvt 為true,私有cache 刪除
        if ( !isEmptyDataObject( cache[ id ] ) ) {
            return;
        }
    }

    // 3. pvt 為 true, 刪除私有 cache
    // 3.1 為節點時,若cache[events]裡還有事件,把 elem 繫結的事件函式刪除
    if ( isNode ) {
        jQuery.cleanData( [ elem ], true );

    // 3.2 普通物件時
    } else if ( support.deleteExpando || cache != cache.window ) {
        // 能刪則 delete
        delete cache[ id ];

    // 不能刪, undefined
    } else {
        cache[ id ] = undefined;
    }
}


// #6192, 函式內包含 刪除事件佇列時刪除elem對應type的繫結事件 的功能
// cleanData 不僅被 internalRemoveData 內部呼叫,remove節點的時候也$().remove呼叫,因此支援了elems 陣列
jquery.cleanData = function( elems, /* internal */ forceAcceptData ) {
    var elem, type, id, data,
        i = 0,
        internalKey = jQuery.expando,
        cache = jQuery.cache,
        attributes = support.attributes,
        special = jQuery.event.special;

    // 支援 elems 陣列迭代
    for ( ; ( elem = elems[ i ] ) != null; i++ ) {
        // 鉤子
        if ( forceAcceptData || acceptData( elem ) ) {

            id = elem[ internalKey ];
            data = id && cache[ id ];

            if ( data ) {
                // 1. 存在事件佇列
                if ( data.events ) {

                    // 迭代,刪除綁在 elem 上的觸發函式
                    for ( type in data.events ) {

                        // 雖然繫結在data上的事件,都轉換成標準的 eventType
                        // 但標準的 eventtype 可能不被相容
                        // special.setup 鉤子在繫結觸發函式時會hack一次
                        // 需要該方法找到繫結在elem上事件觸發函式的真正型別並刪除
                        if ( special[ type ] ) {
                            jQuery.event.remove( elem, type );

                        // 一般情況,直接刪除
                        } else {
                            jQuery.removeEvent( elem, type, data.handle );
                        }
                    }
                }

                // 2. 不存在(或已刪去)事件佇列
                if ( cache[ id ] ) {

                    delete cache[ id ];

                    // Support: IE<9
                    // IE does not allow us to delete expando properties from nodes
                    // IE creates expando attributes along with the property
                    // IE does not have a removeAttribute function on Document nodes
                    if ( !attributes && typeof elem.removeAttribute !== "undefined" ) {
                        elem.removeAttribute( internalKey );

                    } else {
                        elem[ internalKey ] = undefined;
                    }

                    deletedIds.push( id );
                }
            }
        }
    }
}


/* ---------------------------------- 4. 外觀(介面API) ---------------------------------- */

// #4013
jQuery.extend( {
    // cache、noData 上面已經提前寫了
    cache: {},
    noData: {
        "applet ": true,
        "embed ": true,
        "object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
    },

    // 直接從快取 cache 查詢判斷,dom 與 object區別對待
    hasData: function( elem ) {
        elem = elem.nodeType ? jQuery.cache[ elem[ jQuery.expando ] ] : elem[ jQuery.expando ];
        return !!elem && !isEmptyDataObject( elem );
    },

    // 公用,pvt 無
    data: function( elem, name, data ) {
        return internalData( elem, name, data );
    },

    removeData: function( elem, name ) {
        return internalRemoveData( elem, name );
    },

    // 私用,pvt true
    _data: function( elem, name, data ) {
        return internalData( elem, name, data, true );
    },

    _removeData: function( elem, name ) {
        return internalRemoveData( elem, name, true );
    }
} );

// #4049,例項方法
jQuery.fn.extend( {
    data: function( key, value ) {
        var i, name, data,
            elem = this[ 0 ],
            attrs = elem && elem.attributes;

        // Special expections of .data basically thwart jQuery.access,
        // so implement the relevant behavior ourselves

        // 1. key不存在。獲得 data快取 所有值
        if ( key === undefined ) {
            if ( this.length ) {
                data = jQuery.data( elem );

                if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) {
                    i = attrs.length;
                    while ( i-- ) {

                        // Support: IE11+
                        // The attrs elements can be null (#14894)
                        if ( attrs[ i ] ) {
                            name = attrs[ i ].name;
                            if ( name.indexOf( "data-" ) === 0 ) {
                                name = jQuery.camelCase( name.slice( 5 ) );

                                // 鉤子,data[name]若無,則搜尋data- attribute,並賦值給 data[ name ]
                                dataAttr( elem, name, data[ name ] );
                            }
                        }
                    }
                    jQuery._data( elem, "parsedAttrs", true );
                }
            }

            return data;
        }

        // 2. key 是 "object"
        // internalData 已經支援了 "object" 引數,因此直接迭代
        if ( typeof key === "object" ) {
            return this.each( function() {
                jQuery.data( this, key );
            } );
        }

        return arguments.length > 1 ?

            // 3. 迭代寫入 key -> value
            this.each( function() {
                jQuery.data( this, key, value );
            } ) :

            // 4. 讀取 key 屬性值, 沒有則嘗試讀data- attribute,並賦值給 data[ name ]
            elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : undefined;
    },

    removeData: function( key ) {
        return this.each( function() {
            jQuery.removeData( this, key );
        } );
    }
} );



最後再說幾點:

  1. $(elem).data("key", "name")$.data($(elem), "key", "name") 的區別是前者內部元素被迭代,繫結在元素(dom)上,而後者繫結在 $(elem) 物件(object)上,區別不言自明。

  2. 對於支援物件等多種引數形式的邏輯本身更多放在外觀裡,這裡在 internalData,因為公有私有不止一個外觀,避免重複要麼抽出類似 access 使用,要麼放到公共方法中。而之所以不放在最終的例項 data 方法中,因為工具方法已經在jq模組內部被多次使用了,這樣可以有效簡化內部操作。

  3. dataAtrr 之所以在例項 data 方法中才出現被使用,是因為只有使用者呼叫的時候dom才載入完呀,才會產生這個需要。