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對象。
這篇文章主要分為以下知識
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.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.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;
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.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);
});
}
});
其他實現
這些實現都是在司徒正美的《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];
}
//其他代碼略
});
引用
如果你覺得這篇文章不錯,請隨手點一下右下角的“推薦”,舉手之勞,卻鼓舞人心,何樂而不為呢?
