數據緩存系統最早應該是jQuery1.2引入的,那時它的事件系統完成照搬DE大神的addEvent.js,而addEvent在實現有個缺憾,它把事件的回調都放到EventTarget之上,這會引發循環引用,如果EventTarget是window對象,又會引發全局污染。有了數據緩存系統,除了規避這兩個風險外,我們還可以有效地保存不同方法產生的中間變量,而這些變量會對另一個模塊的方法有用,解耦方法間的依賴。對於jQuery來說,它的事件克隆乃至後來的列隊實現都是離不開緩存系統。
jQuery1.2 在core模塊新增了兩個靜態方法, data與removeData。data不用說,與jQuery其他方法一樣,讀寫結合。jQuery的緩存系統是把所有數據都放$.cache之上,然後爲每個要使用緩存系統的元素節點,文檔對象與window對象分配一個UUID。UUID的屬性名爲一個隨機的自定義屬性,"jQuery" + (new Date()).getTime(), 值爲整數,從零遞增。但UUID總要附於一個對象上,如果那個對象是window,豈不是全局污染嗎,因此jQuery內部判定它是window對象時,映射爲一個叫windowData的空對象,然後UUID加在它之上。有了UUID,我們在首次訪問緩存系統時,會在$.cache對象開闢一個空對象(緩存體),用於放置與目標對象有關的東西。這有點像銀行開戶了,UUID的值就是存摺。removeData則會刪掉不再需要保存數據,如果到最後,數據刪清光了,它也沒有任何鍵值對,成爲空對象,jQuery就會從$.cache中刪掉此對象,並從目標對象移除UUID。
//jQuery1.2.3 var expando
= "jQuery" +
( new Date()).getTime(),
uuid = 0, windowData = {}; jQuery.extend({ cache:
{}, data:
function (
elem, name, data ) { elem
= elem == window ? windowData : elem; //對window對象做特別處理 var id
= elem[ expando ]; if (
!id ) //如果沒有UUID則新設一個 id
= elem[ expando ] = ++uuid; //如果沒有在$.cache中開戶,則先開戶 if (
name && !jQuery.cache[ id ] ) jQuery.cache[
id ] = {}; //
第三個參數不爲undefined時,爲寫操作 if (
data != undefined ) jQuery.cache[
id ][ name ] = data; //如果只有一個參數,則返回緩存對象,兩個參數則返回目標數據 return name
? jQuery.cache[ id ][ name ] : id; }, removeData:
function (
elem, name ) { elem
= elem == window ? windowData : elem; var id
= elem[ expando ]; if (
name ) { //移除目標數據 if (
jQuery.cache[ id ] ) { delete jQuery.cache[
id ][ name ]; name
= "" ; for (
name in jQuery.cache[
id ] ) break ; //遍歷緩存體,如果不爲空,那name會被改寫,如果沒有被改寫,則!name
爲true, //從而引發再次調用此方法,但這次是隻傳一個參數,移除緩存體, if (
!name ) jQuery.removeData(
elem ); } }
else { //移除UUID,但IE下對元素使用delete會拋錯 try { delete elem[
expando ]; }
catch (e){ if (
elem.removeAttribute ) elem.removeAttribute(
expando ); } //註銷賬戶 delete jQuery.cache[
id ]; } } }) |
jQuery在1.2.3中添加了兩個同名的原型方法data與removeData,目的是方便鏈式操作與集化操作。並在data中添加getData, setData的自定義事件的觸發邏輯。
1.3中,數據緩存系統終於獨立成一個模塊data.js(內部開發時的劃分),並添加了兩組方法,命名空間上的queue與dequeue,原型上的queue與dequeue。queue的目的很明顯,就是緩存一組數據,爲動畫模塊服務。dequeue是從一組數據中刪掉一個。
//jQuery1.3 jQuery.extend({ queue:
function (
elem, type, data ) { if (
elem ){ type
= (type || "fx" )
+ "queue" ; var q
= jQuery.data( elem, type ); if (
!q || jQuery.isArray(data) ) //確保儲存的是一個數組 q
= jQuery.data( elem, type, jQuery.makeArray(data) ); else if (
data ) //然後往這個數據加東西 q.push(
data ); } return q; }, dequeue:
function (
elem, type ){ var queue
= jQuery.queue( elem, type ), fn
= queue.shift(); //然後刪掉一個,早期它是放置動畫的回調,刪掉它就call一下, //
但沒有做是否爲函數的判定,估計也沒有寫到文檔中,爲內部使用 if (
!type || type === "fx" ) fn
= queue[0]; if (
fn !== undefined ) fn.call(elem); } }) |
fx模塊animate方法的調用示例:
//each是並行處理多個動畫,queue是一個接一個處理多個動畫 this [
optall.queue === false ?
"each" :
"queue" ]( function (){
/*略*/ }) |
在元素上添加自定義屬性,還會引發一個問題。如果我們對這個元素進行拷貝,就會將此屬性也會複製過去,導致兩個元素都有相同的UUID值,出現數據被錯誤操作的情況。jQuery早期的複製節點實現非常簡單,如果元素的cloneNode方法不會複製事件就使用cloneNode,否則使用元素的outerHTML,或父節點的innerHTML,用clean方法解析一個新元素出來。但outerHTML與innerHTML都會顯式屬性寫在裏面,因此需要用正則把它們清除掉。
//jQuery1.3.2
core.js clone方法 var ret
= this .map( function (){ if (
!jQuery.support.noCloneEvent && !jQuery.isXMLDoc( this )
) { var html
= this .outerHTML; if (
!html ) { var div
= this .ownerDocument.createElement( "div" ); div.appendChild(
this .cloneNode( true )
); html
= div.innerHTML; } return jQuery.clean([html.replace(/
jQuery\d+= "(?:\d+|null)" /g,
"" ).replace(/^\s*/,
"" )])[0]; }
else return this .cloneNode( true ); }); |
jQuery1.4發現IE如果對於object, ember, applet這三個古老的用於接入外部資源的標籤可能會拋錯。由於舊式IE的元素節點只是COM的包裝,一旦引入資源後,它就會變成那種資源的實例,而它們會有嚴格的訪問控制,不能像普通的JS對象那樣隨意添加成員。於是jQuery便一刀換,但凡是這三種標籤,就不爲它緩存數據。jQuery弄了一個叫noData的hash,用於檢測元素節點的標籤。
noData:
{ "embed" :
true , "object" :
true , "applet" :
true }, //代碼防禦
if (
elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) { return ; } |
jQuery1.4還對$.data進行改進,允許第二個參數爲對象,方便儲存多個數據。UUID對應的自定義屬性expando 也放進命名空間之下了。queue與dequeue方法被剝離成一個新模塊。
jQuery1.43帶來三項改進。
首先是添加changeData自定義方法。不過這套方法沒有什麼銷量,只是產品經理的自戀吧。
檢測元素節點是否支持添加自定義屬性的邏輯被獨立成一個叫acceptData的方法。因爲jQuery團隊發現當object標籤加載的flash資源,它還是可以添加自定義屬性的,於是決定對這種情況網開一面。IE在加載flash時,需要對object指定一個叫classId的屬性,值爲clsid:D27CDB6E-AE6D-11cf-96B8-444553540000,因此檢測邏輯就變得非常複雜,由於data, removeData都要用到,獨立出來有效節省比特。
HTML5對人們隨便添加自定義屬性的行爲做出迴應,新增一種叫"data-*"的緩存機制。當用戶設置的屬性以"data-"開頭,它們會被保存到元素節點的dataset對象上。這就導致人們可能用HTML5方便緩存數據,也可能用jQuery的緩存系統保存數據,那麼data方法就變得有點不中用了。於是jQuery在原型上的data做了增強,當用戶第一次訪問此元素節點,會遍歷它所有"data-"開頭的自定義屬性(爲了照顧舊式IE,不能直接遍歷dataset),把它們放到jQuery的緩存體中。那麼當用戶取數據時,會先從緩存系統中,沒有再使用setAttribute訪問"data-"自定義屬性。但HTML5的緩存系統非常弱,只能保存字符串(這當然是出於循環引用的考量),於是jQuery會將它們還原爲各種數據類型,如"null",, "false", "true"變成null, false, true, 符合數字格式的字符串會轉換成數字,如果它是以"{"開頭"}"結尾則嘗試轉成一個對象。
//jQuery1.43
$.fn.data rbrace
= /^(?:\{.*\}|\[.*\])$/; if (
data === undefined && this .length
) { data
= jQuery.data( this [0],
key ); if (
data === undefined && this [0].nodeType
=== 1 ) { data
= this [0].getAttribute(
"data-" +
key ); if (
typeof data
=== "string" )
{ try { data
= data === "true" ?
true : data
=== "false" ?
false : data
=== "null" ?
null : !jQuery.isNaN(
data ) ? parseFloat( data ) : rbrace.test(
data ) ? jQuery.parseJSON( data ) : data; }
catch (
e ) {} }
else { data
= undefined; } } } |
jQuery1.5也帶來三項改進。當時jQuery已經在1.42打敗Prototype.js,如日中天,馬太效應,用戶量暴增。它的重點改爲提升性能,進入fix bug階段(用戶多,相當於免費的測試員就越多,測試覆蓋面就越大)。
改進expando,原來是基於時間截,現在是版本號加隨機數。因此用戶可能在一個頁面引入多個版本的jQuery。
是否有此數據的邏輯被抽出成一個hasData方法,處理HTML5的"data-*"屬性也被抽出成一個私有方法dataAttr。它們都是爲了邏輯顯得更清晰。dataAttr使用JSON.parse,由於這個JSON可能是JSON2.js引入的,而JSON2.js有個非常糟糕的地方,就是爲一系列原生類型添加了toJSON方法,導致for in 循環判定是否爲空對象出錯。jQuery被逼搞了個isEmptyDataObject方法做處理。
jQuery的數據緩存系統本來就是爲事件系統服務而分化出來的,到後來,它是內部衆多模塊的基礎設施。換言之,它內部會儲存許多框架用戶的變量(系統數據),但一旦它公開到文檔中,用戶也會使用data保存他們務業中使用的數據(用戶數據)。以前,用戶小,變量名衝突的可能性比較少,加之jQuery爲這些系統數據精挑了一些不常用的名字,__class__, __change__或加個後綴什麼的,沒有收到什麼投訴。當jQuery成爲世界級的著名框架後,用戶數據名幹掉系統數據名,導致事件系統或其他什麼模塊癱瘓就時有發生。jQuery開始對緩存體進行改造,原來就是一個對象,什麼數據都往裏面拋。現在它就這個緩存體內開闢一個子對象,鍵名爲隨機的jQuery.expando值,如果是系統數據就存到裏面去。但events系統數據爲了向前兼容起見,還是直接放到緩存體之上。至於,如何區分是系統數據,非常簡單,直接在data方法添加第四個參數,真值時爲系統數據。removeData時也相應提供第三個參數,用於刪除系統數據。還新設了一個_data方法,專門用於操作系統數據。下面就是緩存體的結構圖:
var cache
= { jQuery14312343254:{ /*放置系統數據*/ } events:
{/ "放置事件名與它對應的回調列表" /} /*這裏放置用戶數據*/ } |
jQuery1.7對緩存體做了改進,系統變量變放置data對象中,爲此判定緩存體爲空也要做相應的改進,現在要跳過toJSON與data。新結構如下:
var cache
= { data:{ /*放置用戶數據*/ } /*這裏放置系統數據*/ } |
jQuery1.8曾添加一個叫deleteIds的數組,用於重用UUID。UUID的值從1.8起不用jQuery.uuid的了,改用jQuery.guid遞增生成。重大的改進在jQuery1.83後,操作數據的實現被抽出爲私有方法,命名空間與原型上的方法只是一個代理,並分成兩組方法,操作用戶數據的data, removeData,操作系統數據的_data,_removeData。現在光是緩存系統就是一個龐大家族了。
說到底,數據緩存就是在目標對象與緩存體間建立一對一的關係,然後在緩存體上操作數據,複雜度都集在前者。而在一個普通JS對象進行增刪改查某屬性從來沒有難度,用戶怎麼也玩不出花招。從軟件設計原則上看,這也是最好的結果(吻合KISS原則與職責單一則)。
如何使用?
//實例方法 $(node).data( "xxx" , "yyy" ) //寫方法,一次寫入一個 $(node).data({ //一次寫入多個 "aaa" :1, "bbb" :2 }) //靜態方法 $.data(node, "ccc" ,3) $._data(node, "ccc" ,3) //由於是放在不同對象上,因此不會覆蓋上面的cc $(node).data( "xxx" ); //讀方法 $.data(node,
"xxx" ) $._data(node,
"ccc" ) $.removeData(node,
"xxx" ) $._removeData(node,
"ccc" )<pre
class= "brush:js;gutter:false;toolbar:false;" > </pre> |