2017年3月17日 星期五

jq_1


jQuery源碼解析(1)—— jq基礎、data緩存系統

標籤: jquery源碼data緩存jq基礎
 1048人閱讀 評論(1) 收藏 舉報
 分類:
目錄(?)[+]

閒話

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 );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
[源碼]
(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;
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

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 );     
    }
    // 具體邏輯
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
[源碼]
// #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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

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 )
    );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34


看到這,說明你真的是個有耐心的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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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]] = {};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
通過 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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

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 );
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
但是,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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
  • 424
  • 425
  • 426
  • 427
  • 428
  • 429
  • 430
  • 431
  • 432
  • 433
  • 434
  • 435
  • 436
  • 437
  • 438
  • 439
  • 440
  • 441
  • 442
  • 443
  • 444
  • 445
  • 446
  • 447
  • 448
  • 449
  • 450
  • 451
  • 452
  • 453
  • 454
  • 455
  • 456
  • 457
  • 458
  • 459
  • 460
  • 461
  • 462
  • 463
  • 464
  • 465
  • 466
  • 467
  • 468
  • 469
  • 470
  • 471
  • 472
  • 473
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
  • 424
  • 425
  • 426
  • 427
  • 428
  • 429
  • 430
  • 431
  • 432
  • 433
  • 434
  • 435
  • 436
  • 437
  • 438
  • 439
  • 440
  • 441
  • 442
  • 443
  • 444
  • 445
  • 446
  • 447
  • 448
  • 449
  • 450
  • 451
  • 452
  • 453
  • 454
  • 455
  • 456
  • 457
  • 458
  • 459
  • 460
  • 461
  • 462
  • 463
  • 464
  • 465
  • 466
  • 467
  • 468
  • 469
  • 470
  • 471
  • 472
  • 473


最後再說幾點:
  1. $(elem).data("key", "name") 與 $.data($(elem), "key", "name") 的區別是前者內部元素被迭代,綁定在元素(dom)上,而後者綁定在 $(elem)對象(object)上,區別不言自明。
  2. 對於支持對象等多種參數形式的邏輯本身更多放在外觀裡,這裡在 internalData,因為公有私有不止一個外觀,避免重複要麼抽出類似 access 使用,要麼放到公共方法中。而之所以不放在最終的實例 data 方法中,因為工具方法已經在jq模塊內部被多次使用了,這樣可以有效簡化內部操作。
  3. dataAtrr 之所以在實例 data 方法中才出現被使用,是因為只有用戶調用的時候dom才加載完呀,才會產生這個需要。