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]; } //其他代碼略 });
引用
如果你覺得這篇文章不錯,請隨手點一下右下角的“推薦”,舉手之勞,卻鼓舞人心,何樂而不為呢?