jQuery源碼解讀 - 數據緩存系統:jQuery.data


jQuery在1.2后引入jQuery.data(數據緩存系統),主要的作用是讓一組自定義的數據可以DOM元素相關聯——淺顯的說:就是讓一個對象和一組數據一對一的關聯。

一組和Element相關的數據如何關聯着這個Element一直是web前端的大姨媽,而最初的jQuery事件系統照搬Dean Edwards的addEvent.js:將回調掛載在EventTarget上,這樣下來,循環引用是不可忽視的問題。而在web前端中,數據和DOM的關系太過基情和緊張,於是jQuery在1.2中,正式締造了jQuery.data,就是為了解決這段孽緣:自定義數據和DOM進行關聯。

文中所說的Element主要是指數據掛載所關聯的target(目標),並不局限於Element對象。

本文原創於linkFly原文地址

這篇文章主要分為以下知識

jQuery.data模型

模型

凡存在,皆真理——任何一樣事物的存在必然有其存在的理由,於我們的角度來說,這叫需求。

一組數據,如何與DOM相關聯一直是web前端的痛處,因為瀏覽器的兼容性等因素。最初的jQuery事件系統照搬Dean Edwards的addEvent.js:將回調掛載在EventTarget上,這樣下來,循環引用是不可忽視的問題,它把事件的回調都放在相應的EventTarget上,當回調中再引用EventTarget的時候,會造成循環引用。於是締造了jQuery.data,在jQuery.event中通過jQuery.data掛載回調函數,這樣解決了回調函數的循環引用,隨時時間的推移,jQuery.data應用越來越廣,例如后來的jQuery.queue

首先我們要搞清楚jQuery.data解決的需求,有一組和DOM相關/描述Element的數據,如何存放和掛載呢?可能有人是這樣的:

使用attributes

HTML:

<div id="demo" userData="linkFly"></div>

javascript:

(function () {
            var demo = document.getElementById('demo');
            console.log(demo.getAttribute('userData'));
})();

使用HTML5的dataset

HTML:

<div id="demo2" data-user="linkFly"></div>

javascript:

        (function () {
            var demo = document.getElementById('demo2');
            console.log(demo.dataset.user);
        })();

為DOM實例進行擴展

HTML:

<div id="demo3"></div>

javascript:

        (function () {
            var demo = document.getElementById('demo3');
            demo.userData = 'demo';
            console.log(demo.userData);
        })();
雖然有解決方案,但都不是理想的解決方案,每個方案都有自己的局限性:
  • 1、只能保存字符串(或轉化為字符串類型)的數據,同時曝露了數據,並且在HTML上掛載了無謂的屬性,瀏覽器仍然會嘗試解析這些屬性。
  • 2、同上。
  • 3、典型的污染,雖然可以保存更強大的數據(Object/Function),但是患有代碼潔癖的騷年肯定是不能忍的,更主要,如果掛載的數據中引用着這個Element,則會循環引用。
jQuery.data,則是為了解決這樣的自定義數據掛載問題。

 

模型

一窺模型吧,jQuery.data在早期,為了兼容性做了很多的事情。同時,或許是因為jQuery.data最初的需求作者也覺得太過簡單,所以實現的代碼上讓人覺得略顯倉促,早期的數據倉庫很是繁瑣,在jQuery.2.x后,jQuery.data重寫,同時終於把jQuery.data抽離出對象。

jQuery.data模型上,就是建立一個數據倉庫,而每一個掛載該數據的對象,都有自己的鑰匙,他和上面的代碼理念並不同:

  • 上面的方案是:

    在需要掛載數據的對象上掛載數據,就好像你身上一直帶着1000塊錢,要用的時候直接從口袋里掏就可以了。

  • jQuery.data則是:

    建立一個倉庫,所有的數據都放在這個倉庫里,然后給每個需要掛載數據的對象一把鑰匙,讀取數據的時候拿這個鑰匙到倉庫里去拿,就好像所有人都把錢存在銀行里,你需要的時候則拿着銀行卡通過密碼去取錢。

圖一張:

我們暫時先不討論數據倉庫的樣子,首先我們要關注數據和Element關聯的關鍵點——鑰匙,這個鑰匙頗具爭議,后續的幾種數據緩存方式都是在對這個鑰匙進行大的變動,因為這個鑰匙,不得不放在Element上——即使你把所有的錢都存在銀行里了,但是你身上還是要有相應的鑰匙,這不得不讓那些代碼潔癖的童鞋面對這個問題:Element注定要被污染——jQuery.data只是嘗試了最小的污染。

jQuery在創建的時候,會生成一個屬性——jQuery.expando

expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" );

jQuery.expando是當前頁面中引用的jQuery對象的身份標志(id),每個頁面引用的jQuery.expando都是不重復且唯一的,所以這就是鑰匙的關鍵:jQuery.expando生成的值作為鑰匙,掛載在Element上,也就是為Element創建一個屬性:這個屬性的名稱,就是jQuery.expando的值,這就是鑰匙的關鍵。 雖然仍然要在Element上掛載自己的數據,但是jQuery盡可能做到了最小化影響用戶的東西。

當然這里需要注意:通過為Element添加鑰匙的時候,使用的是jQuery.expando的值作為添加的屬性名,頁面每個使用過jQuery.data的Element上都有jQuery.expando的值擴展的屬性名,也就是說,每個使用過jQuery.data的Element都有這個擴展的屬性,通過檢索這個屬性值來找到倉庫里的數據——鑰匙是這個屬性值,而不是這個jQuery.expando擴展的屬性名。

木圖木真相:

jQuery.1.x(截至jQuery.1.11)中,內部數據和外部數據掛載在jQuery.cache不同的地方——內部數據掛載在jQuery.cache[鑰匙]下,而用戶數據則掛載在jQuery.cache[鑰匙].data下,原因是因為內部數據如何是用戶數據掛載在一起則會存在相互覆蓋的情況,要把數據給隔離開。

 

jQuery.1.x中jQuery.data實現

這里的jQuery.1.x主要是指jQuery.1.11

jQuery.acceptData() - 目標過濾

因為jQuery.1.x是兼容低版本瀏覽器的,所以需要處理大量的瀏覽器兼容性,在jQuery.1.x中設計的jQuery.data是基於給目標添加屬性來實現的,所以這其中找屬性找鑰匙找倉庫很是繁瑣,再加上IE低版本各種雷區,簡直喪心病狂了已經。找鑰匙找倉庫還好說,低版本IE的雷區一踩一個爆:所以jQuery單獨寫了一個jQuery.acceptData用於屏蔽雷區,特別針對下面的情況:

  • applet和embed:這兩個標簽都是加載外部資源的,這兩個標簽在js里可以操作的權限簡直就是縮卵了——根本行不通,所以jQuery直接給干掉了,直接讓他們不能放標簽。
  • flash:早期的jQuery將所有的Object標簽納入雷區,后來發現IE下的flash還是可以自定義屬性的,於是針對IE的flash還是網開一面,放過了IE的flash,IE下加載flash的時候,需要對object指定一個叫做classId的屬性,它的值為:clsid:D27CDB6E-AE6D-11cf-96B8-444553540000

jQuery.acceptData配合jQuery.noData做的過濾:

    jQuery.extend({
        //jQuery.cache對象,倉庫
        cache: {},
        noData: {
            //有可能權限不夠,所以過濾
            "applet ": true,
            "embed ": true,
            //ie的flash可以通過
            "object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
        }
        //其余API代碼略
    });

    jQuery.acceptData = function (elem) {
        //確定一個對象是否允許設置Data 
        var noData = jQuery.noData[(elem.nodeName + " ").toLowerCase()],
		nodeType = +elem.nodeType || 1;
        //進行過濾
        return nodeType !== 1 && nodeType !== 9 ?
		false :
        //如果是jQuery.noData里面限定的節點的話,則返回false
        //如果節點是object,則判定是否是IE flash的classid
	!noData || noData !== true && elem.getAttribute("classid") === noData;
    };

internalData() - 掛載/讀取數據

掛載和讀取數據方法是同一個(下面有分步分析):首先拿到鑰匙,也就是jQuery.expando擴展的屬性,然后根據鑰匙獲取倉庫,因為內部數據和用戶數據都是掛載在jQuery.cache下的,所以在jQuery.cache下開辟了jQuery.cache[鑰匙].data作為用戶數據存放的空間,而jQuery.cache[鑰匙]則存放jQuery的內部數據,將數據掛上之后,返回的結果是這個數據掛載的空間/位置,通過這個返回值可以訪問到這個Element所有掛載的數據。

 function internalData(elem, name, data, pvt /* Internal Use Only */) {
        //pvt:標識是否是內部數據

        //判定對象是否可以存數據
        if (!jQuery.acceptData(elem)) {
            return;
        }
        var ret, thisCache,
              //來自jQuery隨機數(每一個頁面上唯一且不變的)
		    internalKey = jQuery.expando,
            /*
    		    如果是DOM元素,
		        為了避免javascript和DOM元素之間循環引用導致的瀏覽器(IE6/7)垃圾回收機制不起作用,
		        要把數據存儲在全局緩存對象jQuery.cache中
            */
		    isNode = elem.nodeType,
		    //只有DOM節點才需要全局緩存,js對象是直接連接到對象的
            //如果是DOM,則cache連接到jQuery.cache
		    cache = isNode ? jQuery.cache : elem,
		    //如果是DOM,則獲取鑰匙,如果是第一次讀取,則讀取不到鑰匙
		    id = isNode ? elem[internalKey] : elem[internalKey] && internalKey;
        //檢測合法性,避免做更多的工作,pvt標識是否是內部數據
        if ((!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string") {
            return;
        }
        //沒有拿到鑰匙,則賦上ID
        if (!id) {
            if (isNode) {
                /*
                   DOM需要有一個全新的全局id,
                   為DOM建立一個jQuery的全局id
                   低版本代碼:elem[ internalKey ] = id = ++jQuery.uuid;
                   這個deletedIds暫時忽略,當初jQuery准備重用uuid,后來被guid取代了
                */
                id = elem[internalKey] = deletedIds.pop() || jQuery.guid++;
            } else {
                //對象就不用這么麻煩了,直接掛鑰匙就可以了
                id = internalKey;
            }
        }
        //從jQuery.cache中沒有讀取到,則開辟一個新的
        if (!cache[id]) {
            //創建一個新的cache對象,這個toJson是個空方法
            cache[id] = isNode ? {} : { toJSON: jQuery.noop };
            /*
              對於javascript對象,設置方法toJSON為空函數,
              以避免在執行JSON.stringify()時暴露緩存數據。
              如果一個對象定義了方法toJSON()
              JSON.stringify()在序列化該對象時會調用這個方法來生成該對象的JSON元素
            */
        }

            /*
              先把Object/Function的類型的數據掛上。調用方式 :
              $(Element).data({'name':'linkFly'});
              這里的判定沒有調用jQuery.type()...當然在於作者的心態了...
            */
        if (typeof name === "object" || typeof name === "function") {
            if (pvt) {//如果是內部數據
                //掛到cache上
                cache[id] = jQuery.extend(cache[id], name);
            } else {
                //如果是自定義數據,則掛到data上
                cache[id].data = jQuery.extend(cache[id].data, name);
            }
        }
        //調整位置,因為有可能是取數據
        thisCache = cache[id];

        if (!pvt) {
            //如果不是內部數據(即用戶自定義數據),則調整到jQuery.chche.data上
            if (!thisCache.data) {
                thisCache.data = {};
            }
           //繼續調整位置
            thisCache = thisCache.data;
        }
        /*
             到了這里外面的調用方式是
             $(Element).data('name','value');
        */
        if (data !== undefined) {
            //掛上數據
            thisCache[jQuery.camelCase(name)] = data;
        }
       if (typeof name === "string") {
            ret = thisCache[name];
            if (ret == null) {
                ret = thisCache[jQuery.camelCase(name)];
            }
        } else {
           ret = thisCache;
        }

        //同時處理獲取數據的情況
        return ret;
    }        

太長看起來惡心?來,我們一點點分析:

1、首先,檢測是否可以存放數據,可以的話初始化操作,針對變量id要注意,這里的一直在找上面我們所說的掛載在Element上那個存放鑰匙的屬性,也就是jQuery.expando的值

        if (!jQuery.acceptData(elem)) {
            return;
        }
        var ret, thisCache,
             //來自就jQuery隨機數(每一個頁面上唯一且不變的)
		    internalKey = jQuery.expando,
            /*
        	    如果是DOM元素,
		        為了避免javascript和DOM元素之間循環引用導致的瀏覽器(IE6/7)垃圾回收機制不起作用,
		        要把數據存儲在全局緩存對象jQuery.cache中;
		        對於javascript對象有垃圾回收機制
		        所以不會有內存泄露的問題
		        因此數據可以直接存儲在javascript對象上
            */
		    isNode = elem.nodeType,
            //如果是Element,則cache連接到jQuery.cache
		    cache = isNode ? jQuery.cache : elem,
		    //如果是Element,則獲取鑰匙
		    id = isNode ? elem[internalKey] : elem[internalKey] && internalKey;
        //檢測合法性,第一次調用$(Element).data('name')會被攔截
        if ((!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string") {
            return;
        }

2、如果沒有鑰匙(id),則為目標添加上鑰匙,代碼如下:

        //沒有ID,則賦上ID
        if (!id) {
            if (isNode) {
               /*
                DOM需要有一個全新的全局id
                為DOM建立一個jQuery的全局id
                低版本代碼:elem[ internalKey ] = id = ++jQuery.uuid;
                這個deletedIds暫時忽略
                id = elem[internalKey] = deletedIds.pop() || jQuery.guid++;
                */
            } else {
                //而對象不需要
                id = internalKey;
            }
        }

2、根據鑰匙,嘗試從cache中讀倉庫的位置,如果倉庫中還沒有這個目標的存放空間,則開辟一個,這里特別針對了JSON做了處理:當調用JSON.stringify序列化對象的時候,會調用這個對象的toJSON方法,為了保證jQuery.data里面數據的安全,所以直接重寫toJSON為一個空方法(jQuery.noop),這樣就不會曝露jQuery.data里面的數據。另外一種說法是針對HTML5處理的dataAttr()(下面有講)使用JSON.parse轉換Object對象,而這個JSON可能是JSON2.js引入的:JSON2.js會為一系列原生類型添加toJSON方法,導致for in循環判定是否為空對象的時候無法正確判定——所以jQuery搞了個jQuery.noop來處理這里。

        //從cache中沒有讀取到
        if (!cache[id]) {
            //創建一個新的cache對象,這個toJson是個空方法
            cache[id] = isNode ? {} : { toJSON: jQuery.noop };
            /*
                對於javascript對象,設置方法toJSON為空函數,
                以避免在執行JSON.stringify()時暴露緩存數據。
                如果一個對象定義了方法toJSON()
                JSON.stringify()在序列化該對象時會調用這個方法來生成該對象的JSON元素
            */
        }

3、如果是Function/Object,則直接調用jQuery.extend掛數據,把$(Element).data({'name':'linkFly'})這種調用方式的數據掛到jQuery.cache

        /*
          先把Object/Function的類型的數據掛上。調用方式 :
          $(Element).data({'name':'linkFly'});
          這里的判定沒有調用jQuery.type()...當然在於作者的心態了...
        */

        if (typeof name === "object" || typeof name === "function") {
            if (pvt) {//如果是內部數據
                //掛到cache上
                cache[id] = jQuery.extend(cache[id], name);
            } else {
                //如果是自定義數據,則掛到data上
                cache[id].data = jQuery.extend(cache[id].data, name);
            }
        }

4、還有其他數據類型(String之類的)沒有掛載上,這里把$(Element).data('name','value')的數據掛載上,最后要把這個data作為方法的返回值,這個返回值非常重要,從而實現也可以讀取數據的功能

        //有可能是獲取數據,所以開始調整位置
        thisCache = cache[id];
        //調整返回值的位置
        if (!pvt) {
            //如果不是內部數據(即用戶自定義數據),則掛到jQuery.chche.data上
            if (!thisCache.data) {
                thisCache.data = {};
            }

            thisCache = thisCache.data;
        }
        /*
                如果是這樣調用的:$(Element).data('name','value');
                那么剛好利用上面的thisCache(當前指向要掛載的空間)
                把數據掛上去
        */
        if (data !== undefined) {
            thisCache[jQuery.camelCase(name)] = data;
        }
        //數據全部掛好,調整返回值
        if (typeof name === "string") {
            //如果參數是一個字符串
            //則抓取這個字符串對應的數據
            ret = thisCache[name];

            //抓取失敗,轉換成駝峰再抓
            if (ret == null) {
            }
        } else {
            //如果參數是Object/Function,則直接返回
            ret = thisCache;
        }

        //最終返回
        return ret;
代碼上非常嚴謹,每一步都盡可能寫在最恰當的地方,這里的方法會在jQuery最外層的API中和dataAttr()(下面會講)方法一起配合來實現掛載/讀取數據。

internalRemoveData() - 移除數據

數據移除方法移除數據方便比較簡單,但是當倉庫中沒有相應Element存儲的數據的時候,會直接從倉庫中刪除這個存儲空間(下面有分步分析):

function internalRemoveData(elem, name, pvt) {
        //移除一個data到jQuery緩存中
        if (!jQuery.acceptData(elem)) {
            return;
        }

        var thisCache, i,
		isNode = elem.nodeType,
		cache = isNode ? jQuery.cache : elem,
		id = isNode ? elem[jQuery.expando] : jQuery.expando;
        if (!cache[id]) {
            return;
        }

        if (name) {
            //獲取數據
            thisCache = pvt ? cache[id] : cache[id].data;

            if (thisCache) {
                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 {
                    //jQuery.map將一個類數組轉轉換成真正的數組
                    //注意這里使用了連接,即如果刪除失敗則采用駝峰命名再次嘗試刪除,邏輯好嚴謹
                    name = name.concat(jQuery.map(name, jQuery.camelCase));
                }

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

                //如果是剩下的緩存中沒有數據了,則完成了任務,否則繼續
                if (pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache)) {
                    return;
                }
            }
        }

        //如果不是jQuery內部使用
        if (!pvt) {
            delete cache[id].data;// data也刪除
            //檢測還有沒有數據,還有數據則繼續
            if (!isEmptyDataObject(cache[id])) {
                return;
            }
        }

        //如果是Element,則破壞緩存
        if (isNode) {
            jQuery.cleanData([elem], true);
        } else if (support.deleteExpando || cache != cache.window) {
            //不為window的情況下,或者可以瀏覽器檢測可以刪除window.屬性,再次嘗試刪除
            //低版本ie不允許刪除window的屬性
            delete cache[id];
        } else {
            //否則,直接null
            cache[id] = null;
        }
    }

1、和internalData()一樣,拿鑰匙。

        //移除一個data到jQuery緩存中
        if (!jQuery.acceptData(elem)) {
            return;
        }

        var thisCache, i,
		    isNode = elem.nodeType,
		    cache = isNode ? jQuery.cache : elem,
                //根據jQuery標識拿鑰匙
		    id = isNode ? elem[jQuery.expando] : jQuery.expando;
        //如果找不到緩存,不再繼續
        if (!cache[id]) {
            return;
        }

2、找到倉庫存儲數據的位置,然后刪除數據,這里充分的考慮了數據命名和Object參數的情況。

if (name) {
            //獲取緩存的位置
            thisCache = pvt ? cache[id] : cache[id].data;
            if (thisCache) {
                if (!jQuery.isArray(name)) {//如果並不具有數組行為
                    if (name in thisCache) {
                        //檢查緩存是否有這個對象
                        name = [name];
                    } else {
                        name = jQuery.camelCase(name);
                        //轉換駝峰再次嘗試
                        if (name in thisCache) {
                            name = [name];
                        } else {
                            /*
                              這樣都還拿不到,那還是按照自己的方式拿把,
                              也就是說jQuery支持
                            $(Element).removeData('name name2 name 3')
                              這樣批量刪除數據,真是被jQuery寵壞了...
                            */
                            name = name.split(" ");
                        }
                    }
                } else {
                    //jQuery.map將一個類數組轉轉換成真正的數組
                    //注意這里使用了連接,即如果刪除失敗則采用駝峰命名再次嘗試刪除,邏輯好嚴謹
                    name = name.concat(jQuery.map(name, jQuery.camelCase));
                    //'name-demo name-demo2'會轉換成
                    //'name-demo name-demo2 nameDemo nameDemo2'
                }

                i = name.length;
                //刪除緩存
                while (i--) {
                    delete thisCache[name[i]];
                }
                /*
                 如果是剩下的緩存中沒有數據了,則完成了任務,否則有不和諧的情況,要繼續處理
                isEmptyDataObject專門用來檢測用戶緩存空間是否是空Data,
                如果緩存空間是這樣的{ test:{'name':'value'} }(用戶數據掛載的空間[data]是空的),就能通過
                */
                if (pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache)) {
                    return;
                }
            }
        }

3、如果數據全部刪除了,那么倉庫存儲數據的空間也要被刪除,所以接下來針對這些情況進行了處理

        //如果不是jQuery內部使用
        if (!pvt) {
            delete cache[id].data;// 連data也刪除
            //檢測還有沒有數據,還有數據則繼續
            if (!isEmptyDataObject(cache[id])) {
                return;
            }
        }

4、因為jQuery.data和jQuery.event事件系統直接掛鈎,所以這里特別針對事件系統掛載的數據進行了刪除處理,jQuery.cleanData方法涉及jQuery.event,所以暫不解讀了。

        //如果是Element,則破壞緩存
        if (isNode) {
            //和jQuery.event掛鈎,不分析了...
            jQuery.cleanData([elem], true);
        } else if (support.deleteExpando || cache != cache.window) {
            //不為window的情況下,或者可以瀏覽器檢測可以刪除window.屬性,再次嘗試刪除
            delete cache[id];
        } else {
            //否則,直接粗暴的null
            cache[id] = null;
        }

dataAttr()和jQuery.fn.data() - 針對HTML5的dataset和曝露API

dataAttr()是特別針對HTML5的dataset進行處理的方法,用處是讀取Element上HTML5的data-*屬性轉換到jQuery.data中,是針對HTML5的兼容,典型的老夫就是要寵死你的方法:

    function dataAttr(elem, key, data) {
        //針對HTML5做一層特別處理,等下在jQuery.fn.data中和internalData()配合使用將會大放異彩

        /*
              注意這里的一層判定,在jQuery.fn.data調用的時候
              會先調用用internalData(),然后把internalData()的返回值傳遞到這里,就是data
              如果data為undefined,則進行HTML5處理
        */
        if (data === undefined && elem.nodeType === 1) {
            /*
                rmultiDash = /([A-Z])/g
                針對HTML5,把駝峰命名的數據轉換為連字符:
                dataSet轉換為data-set
            */
            var name = "data-" + key.replace(rmultiDash, "-$1").toLowerCase();
            data = elem.getAttribute(name);//不用dataset是因為一些比較古老的手機沒有被支持,樓主就被折磨過...
            if (typeof data === "string") {
                //各種喪心病狂的轉換數據
                //把不同的數據類型給轉換成需要的類型
                try {
                    data = data === "true" ? true :
					data === "false" ? false :
					data === "null" ? null :
                    //如果是數字
					+data + "" === data ? +data :
                    //匹配json
					rbrace.test(data) ? jQuery.parseJSON(data) :
					data;
                } catch (e) { }
                //把HTML5的數據掛到jQuery中
                jQuery.data(elem, key, data);

            } else {
                data = undefined;
            }
        }
        //返回這個data,jQuery.fn.data會調用dataAttr()並返回它的值
        return data;
    }

jQuery.data實現很簡單......個屁啊,媽蛋啊看起來就是調用internalData()實現,實際上jQuery.fn.data更加的健壯,同時將各種內層的方法都聯接的惟妙惟肖,當然這也意味着性能更遜色一點,

jQuery.fn.extend({
        data: function (key, value) {
            var i, name, data,
			elem = this[0],
			attrs = elem && elem.attributes;
            //$(Element).data() - 獲取全部數據
            if (key === undefined) {
                //獲取
                if (this.length) {
                    data = jQuery.data(elem);
                    //如果沒有標志parsedAttrs的數據,則表示沒有進行過HTML5的屬性轉換
                    if (elem.nodeType === 1 && !jQuery._data(elem, "parsedAttrs")) {
                        i = attrs.length;
                        while (i--) {
                            //那么轉換HTML5的屬性
                            if (attrs[i]) {
                                name = attrs[i].name;
                                if (name.indexOf("data-") === 0) {
                                    name = jQuery.camelCase(name.slice(5));
                                    //配合dataAttr進行轉換
                                    dataAttr(elem, name, data[name]);
                                }
                            }
                        }
                        //放上屬性parsedAttrs,表示HTML5轉換完畢
                        jQuery._data(elem, "parsedAttrs", true);
                    }
                }

                return data;
            }

            //$(Element).data({ name:'linkFly',value:'hello world' });
            if (typeof key === "object") {
                //循環設置
                return this.each(function () {
                    jQuery.data(this, key);
                });
            }

            return arguments.length > 1 ?
            //$(Element).data('name','linkFly')
			this.each(function () {
			    jQuery.data(this, key, value);
			}) :
                /*
                    使用jQuery.data讀取數據,如果讀取不到,則調用dataAttr()讀取並設置一遍HTML5的數據
                */
			elem ? dataAttr(elem, key, jQuery.data(elem, key)) : undefined;
        }
    });

1、針對獲取全部數據做處理,同時在內部標識上parsedAttrs,表示這個Element已經被轉換過HTML5屬性了:

            if (key === undefined) {
                //獲取
                if (this.length) {
                    data = jQuery.data(elem);
                    //如果沒有標志parsedAttrs的數據,則表示沒有進行過HTML5的屬性轉換
                    if (elem.nodeType === 1 && !jQuery._data(elem, "parsedAttrs")) {
                        i = attrs.length;
                        while (i--) {
                            //那么轉換HTML5的屬性
                            if (attrs[i]) {
                                name = attrs[i].name;
                                if (name.indexOf("data-") === 0) {
                                    name = jQuery.camelCase(name.slice(5));
                                    //配合dataAttr進行轉換
                                    dataAttr(elem, name, data[name]);
                                }
                            }
                        }
                        //放上屬性parsedAttrs,表示HTML5轉換完畢
                        jQuery._data(elem, "parsedAttrs", true);
                    }
                }

                return data;
            }

2、如果不是讀取全部數據,則情況要么是掛載數據,要么是讀取數據,但在最后的一段代碼比較不錯,是internalData()和dataAttr()的配合使用針對HTML5 dataset的兼容:

        if (typeof key === "object") {
                //循環設置
                return this.each(function () {
                    jQuery.data(this, key);
                });
        }

        return arguments.length > 1 ?
            //$(Element).data('name','linkFly')
		this.each(function () {
			jQuery.data(this, key, value);
		}) :
                /*
                    $(Elment).data('name')  
                    這里的代碼很有意思:
                    jQuery.data(elem,key)是調用internalData(),而internalData最終會返回要掛載的數據
                    如果用戶掛載的數據是空的,則調用dataAttr()嘗試轉換HTML5的數據返回並且給掛到jQuery.data
                */
        elem ? dataAttr(elem, key, jQuery.data(elem, key)) : undefined;
}

這里重點照顧最后一句,它實現了:

  • 讀取數據:$(Element).data('demo');
  • 如果讀取不到,讀取HTML5的dataset數據並掛載到jQuery.cache中。

如果到了這里,那么調用方式會是:$(Elment).data('name'),這時候的處理方法就是:

  • jQuery.data底層是internalData(),當第三個參數為空的時候,則是讀取數據
  • internalData()如果讀取不到數據,則調用dataAttr(),而dataAttr第三個參數為undefined的時候,則會讀取HTML5的dataset,然后再調用jQuery.data()(注意不是jQuery.fn.data)再掛一次數據。
好的各位爺,至此 jQuery.1.x代碼已經讀完了,要不您老喝點茶看看窗外放松一下消化一下上面的代碼?:
  • jQuery.expando是鑰匙的關鍵,將jQuery.expando的值掛在Element上,就好像在你身上掛了一張銀行卡,而銀行卡的密碼,則是jQuery.guid(累加不重復)。
  • 通過鑰匙找到倉庫,進行操作。
  • internalData()的思路很值得借鑒,在掛數據的時候同時取數據,尤其在jQuery.cache這個相對比較復雜的環境里,如何更高效的取數據本身就是一件值得思考的事情。
  • internalRemoveData()實現了深度刪除數據,盡可能讓數據仿佛從未存在過,並且嘗試了多種刪除。
  • dataAttr()是針對HTML5特別的兼容處理。
  • internalData()方法非常的嚴謹,但是它仍然只是為了掛載數據和移除數據而生,非常純粹而簡單的工作着,真正讓jQuery健壯的是jQuery.fn.data。

jQuery.2.x中jQuery.data實現

這里的jQuery.2.x主要是指jQuery.2.1

jQuery.2.x中,jQuery.data終於決定被好好深造一下了,過去1.x的代碼說多了都是淚,jQuery.2.x沒有了兼容性的后顧之憂,改寫后的代碼讀起來簡直不要太舒適啊。

jQuery.2.x中,為數據緩存建立了Data對象,一個Data對象表示一個數據倉庫——用戶數據和內部數據各自使用不同的Data對象,這樣就不需要在倉庫里翻來翻去的查找數據存儲的位置了(jQuery.cache[鑰匙]和jQuery.cache[鑰匙].data),思路上,仍然和jQuery.1.x一致,采用擴展屬性的方式實現,關鍵點在Data.prorotype.key()上。

Data對象 - 數據倉庫

Data對象經過封裝以后衍生了這些API:

  • key:專門用來獲取和放置Element的鑰匙。
  • set/get:放置和獲取數據
  • access:通用API,根據參數既然可以放置也可以獲取數據
  • remove:移除數據
  • hasData:檢測是否有數據
  • discard:丟棄掉這個Element的存儲空間

其他的實現都比較簡單,我們需要關注鑰匙這里,也就是Data.prototype.key()

Data.prototype.key() - 鑰匙

Data.prototype = {
        //獲取緩存的鑰匙
        key: function (owner) {
            //檢測是否可以存放鑰匙
            if (!Data.accepts(owner)) {
                return 0; //return false
            }

            var descriptor = {},
                //獲取鑰匙,還是在Element上掛載jQuery屬性
	         unlock = owner[this.expando];
            //如果鑰匙沒有則創建
            if (!unlock) {
                unlock = Data.uid++;
                try {
                    //把expando轉移到Data中,沒一個Data實例都有不同的expando
                    descriptor[this.expando] = { value: unlock };
                    //參考:http://msdn.microsoft.com/zh-cn/library/ie/ff800817%28v=vs.94%29.aspx
                    //這個屬性不會被枚舉
                    Object.defineProperties(owner, descriptor);
                } catch (e) {
                    //如果沒有Object.defineProperties,則采用jQuery.extend
                    descriptor[this.expando] = unlock;
                    jQuery.extend(owner, descriptor);
                }
            }
            if (!this.cache[unlock]) {
                this.cache[unlock] = {};
            }
            //返回這個鑰匙
            return unlock;
        }
    };

因為用戶數據和jQuery內部數據通過Data分離,所以set/get在拿到鑰匙之后都比較簡單。

access() - 通用接口

在創建Data對象的時候,順便為jQuery創建了靜態方法——jQuery.access:通用的底層方法,既能設置也能讀取,它應用在jQuery很多API中,例如:Text()、HTML()等。

var access = jQuery.access = function (elems, fn, key, value, chainable, emptyGet, raw) {
        //元素,委托的方法,屬性名,屬性值,是否鏈式,當返回空數據的時候采用的默認值,fn參數是否是Function
        //一組通用(內部)方法,既然設置也能獲取Data
        var i = 0,
		len = elems.length,
		bulk = key == null;
        //Object
        if (jQuery.type(key) === "object") {
            //如果是放數據,Object類型,則循環執行fn
            chainable = true;//這里修正了是否鏈式....
            for (i in key) {
                jQuery.access(elems, fn, i, key[i], true, emptyGet, raw);
            }

            // Sets one value
        } else if (value !== undefined) {
            chainable = true;
            //如果設置的value是Function
            if (!jQuery.isFunction(value)) {
                raw = true;
            }
            //當參數是這樣的:access(elems,fn,null)
            if (bulk) {
                if (raw) {
                    //參數是這樣的:access(elems,fn,null,function)
                    fn.call(elems, value);
                    fn = null;
                } else {
                    //參數是這樣的:access(elems,fn,null,String/Object)
                    bulk = fn;//這里把fn給調換了
                    fn = function (elem, key, value) {
                        //這個jQuery()封裝的真是....
                        return bulk.call(jQuery(elem), value);
                    };
                }
            }
            //到了這里如果還可以執行的話那么參數是:access(elems,fn,key,Function||Object/String)
            if (fn) {
                for (; i < len; i++) {
                    //循環每一項執行
                    fn(elems[i], key, raw ? value : value.call(elems[i], i, fn(elems[i], key)));
                }
            }
        }

        return chainable ?
            //如果是設置數據,這個elems最終被返回,而在jQuery.fn.data中這個elems是this——也就是jQuery對象,保證了鏈式
            elems :

        // Gets
            //如果上面的設置方法都沒有走,那么就是獲取
		bulk ?//bulk是不同的工作模式,參閱jQuery.css,jQuery.attr
			fn.call(elems) :
			len ? fn(elems[0], key) : emptyGet;
    };

jQuery.fn.data() - 曝露API

相比jQuery.1.x代碼更加的細膩了許多,這里配合着上面定義的access()使用,為每一個循環的jQuery項設置和讀取數據,閱讀起來比較輕松。

jQuery.fn.extend({
        data: function (key, value) {
            var i, name, data,
			elem = this[0],
			attrs = elem && elem.attributes;

            // 獲取全部的數據,和1.x思路一致
            if (key === undefined) {
                if (this.length) {
                    data = data_user.get(elem);

                    if (elem.nodeType === 1 && !data_priv.get(elem, "hasDataAttrs")) {
                        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));
                                    dataAttr(elem, name, data[name]);
                                }
                            }
                        }
                        data_priv.set(elem, "hasDataAttrs", true);
                    }
                }

                return data;
            }

            // 設置Object類型的的數據
            if (typeof key === "object") {
                return this.each(function () {
                    data_user.set(this, key);
                });
            }
            //調用jQuery.access
            return access(this, function (value) {
                //value則是掛載的數據名(即使外面掛載的Object也會被拆開到這里一個個循環執行)
                var data,
		     camelKey = jQuery.camelCase(key);//轉換駝峰
                if (elem && value === undefined) {
                    //拿數據
                    data = data_user.get(elem, key);
                    if (data !== undefined) {
                        return data;
                    }
                    //用駝峰拿
                    data = data_user.get(elem, camelKey);
                    if (data !== undefined) {
                        return data;
                    }
                    //用HTML5拿
                    data = dataAttr(elem, camelKey, undefined);
                    if (data !== undefined) {
                        return data;
                    }
                    return;
                }
                //循環每一項設置
                this.each(function () {
                    //提前設置駝峰的...
                    data_user.set(this, camelKey, value);
                    if (key.indexOf("-") !== -1 && data !== undefined) {
                    //如果有name-name命名再設一邊
                        data_user.set(this, key, value);
                    }
                });
            }, null, value, arguments.length > 1, null, true);
        },

        removeData: function (key) {
            return this.each(function () {
                //調用相應Data實例方法移除即可
                data_user.remove(this, key);
            });
        }
    });
各位看官到了這里可以繼續小憩一下,后面我們再來談談關於這個jQuery.data更多有意思的事情...總結一下, jQuery.2.x的緩存設計理念清晰,最主要的就是封裝成了Data對象以后將用戶數據和jQuery內部使用的數據隔離開,這是最大的改進。移動端的 Zepto里的Zepto.datajQuery.data.2.x的濃縮版。

其他實現

這些實現都是在司徒正美《javascript框架設計》 - "數據緩存系統"一章里讀到的,有必要宣傳和感謝一下這本書,了解了很多代碼的由來促進了理解。

這些實現其實都是針對鑰匙怎么交給Element這個問題上進行的探索。

valueOf()重寫

jQuery.2.x最初設計的jQuery.data中,作者也在為Element掛載這個expando屬性作為鑰匙而頭疼,於是給出了另外一種鑰匙的掛載方法——重寫valueOf()。 Waldron

在為Element掛載鑰匙的時候,不再給這個Element聲明屬性,而是通過重寫Element的valueOf方法實現。

雖然我翻了jQuery.2.0.0 - jQuery.2.1.1都沒有找到這種做法,但覺得還是有必要提一下:

    function Data() {
            this.cache = {};
        };
        Data.uid = 1;
        Data.prototype = {
            locker: function (owner) {
                var ovalueOf,
                    unlock = owner.valueOf(Data);
                /*
                owner為元素節點、文檔對象、window
                傳遞Data類,如果返回object說明沒有被重寫,返回string則表示已被重寫
                整個過程被jQuery稱之為開鎖,通過valueOf得到鑰匙,進入倉庫
                */
                if (typeof unlock !== 'string') {
                    //通過閉包保存,也意味着內存消耗更大
                    unlock = jQuery.expando + Data.uid++;
                    //緩存原valueOf方法
                    ovalueOf = owner.valueOf;
                    Object.defineProperty(owner, 'valueOf', {
                        value: function (pick) {
                            //傳入Data
                            if (pick === Data)
                                return unlock; //返回鑰匙
                            return ovalueOf.apply(owner); //返回原valueOf方法
                        }
                    });
                }
                if (!this.cache[unlock])
                    this.cache[unlock] = {};
                return unlock;
            },
            get: function (owner, key) {
                var cache = this.cache[this.locker(owner)];
                return key === undefined ? cache : cache[key];
            },
            set: function (owner, key, value) {
                //略
            }
            /*其他方法略*/
        };

思路上很是新穎——因為在js中幾乎所有的js數據類型(null,undefined除外)都擁有valueOf/toString方法,所以直接重寫Element的valueOf,在傳入Data對象的時候,返回鑰匙,否則返回原valueOf方法——優點是鑰匙隱性掛到了Element上,保證了Element的干凈和無需再考慮掛屬性兼不兼容等問題了,而缺點就是采用閉包,所以內存消耗更大,或許jQuery也覺得這種做法的內存消耗不能忍,所以仍未采用——相比較放置鑰匙到Element的方式,還是后者更加的純粹和穩定。

Array.prototype.indexOf()

Array.prototype.indexOf()ECMAScript 5(低版本瀏覽器可以使用代碼模擬)定義的方法——可以從一組Array中檢索某項是否存在?存在返回該項索引:不存在則返回-1。聽起來很相似?沒錯,它就是String.prototype.indexOf()的數組版。

正是因為提供了針對數組項的查找,所以可以采用新的思路:

  • 1、將使用data()方法掛載數據的Element通過閉包緩存到一個數組中
  • 2、當下次需要檢索和這個Element關聯的數據的時候,只需要通過Array.ptototype.indexOf在閉包中查找到這個數組即可,而閉包中這個數組查找到的索引,就是鑰匙。

代碼如下:

        (function () {
            var caches = [],
                add = function (owner) {
                    /*

                    //拆開來是這樣子的
                    var length = caches.push(owner);//返回Array的length
                    return caches[length - 1] = {};//新建對象並返回
                    */
                    return caches(caches.push(owner) - 1) = {};
                },
            addData = function (owner, name, data) {
                var index = caches.indexOf(owner), //查找索引,索引即是鑰匙
                //獲取倉庫
                    cache = index === -1 ? add(owner) : caches[index];
                //針對倉庫放數據即可
            }
            //其他代碼略
        })();

這樣就不需要在Element上掛載自定義的屬性(鑰匙)了——然而因為每個使用過data()的Element都會在緩存下來,那么內存的消耗必不可免,相比上一種重寫valueOf重寫消耗更加的不能直視,這是一個有趣但並不推薦的解決方案。

WeakMap

技術總是層出不窮的,對於目前的我們來說可望不可及的ECMAScript 6定義了新的對象——WeakMap,請參考這三篇:

WeakMap對象的鍵值持有其所引用對象的弱引用——當那個對象被垃圾回收銷毀的時候,WeakMap對象相應的鍵值也會被刪除。它使用get/set方法將成員添加到WeakMap——簡直就是為數據緩存系統/jQuery.data量身定做的。使用它,我們Data.key()方法可以改寫成下面的代碼:

    (function () {
            var caches = new WeakMap(),//緩存中心
                addData = function (owner, name, data) {
                    //根據Element獲取相應倉庫存儲的空間
                    var cache = caches.get(owner);
                    //如果獲取不到,則開辟空間
                    if (!cache) {
                        cache = {};
                        //放到WeakMap對象中
                        caches.set(owner, cache);
                    }
                    //掛數據
                    cache[name] = data;
                    return cache;
                },
                removeData = function (owner, name) {
                    var cache = caches.get(owner);
                    //name為undefined的時候返回全部data,否則返回name指定的data
                    return name === undefined ? cache : cache && cache[name];
                }
            //其他代碼略
        });

引用

作者:linkFly
聲明:嘿!你都拷走上面那么一大段了,我覺得你應該也不介意順便拷走這一小段,希望你能夠在每一次的引用中都保留這一段聲明,尊重作者的辛勤勞動成果,本文與博客園共享。

如果你覺得這篇文章不錯,請隨手點一下右下角的“推薦”,舉手之勞,卻鼓舞人心,何樂而不為呢?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM