jQuery的數據緩存模塊以一種安全的方式為DOM元素附加任意類型的數據,避免了在JavaScript對象和DOM元素之間出現迴圈引用,以及由此而導致的記憶體泄漏。 數據緩存模塊為DOM元素和JavaScript對象提供了統一的數據設置、讀取和移除方法,在jQuery內部還為隊列模塊、動畫模塊、樣式操 ...
jQuery的數據緩存模塊以一種安全的方式為DOM元素附加任意類型的數據,避免了在JavaScript對象和DOM元素之間出現迴圈引用,以及由此而導致的記憶體泄漏。
數據緩存模塊為DOM元素和JavaScript對象提供了統一的數據設置、讀取和移除方法,在jQuery內部還為隊列模塊、動畫模塊、樣式操作模塊、事件系統提供基礎功能,負責維護這些模塊運行時的內部數據。
writer by:大沙漠 QQ:22969969
對於DOM元素和JavaScript對象,數據的存儲位置是不同的,如下:
- 對於DOM元素jQuery直接把數據存儲在jQuery.cache中
- 對於JavaScript對象,垃圾回收機制能夠自動發生,因此數據可以直接存儲在JavaScript對象中。
另外為了避免jQuery內部使用的數據和用戶自定義的數據發生衝突,分為內部數據緩存對象和自定義數據緩存對象
- 內部緩存對象 ;jQuery內部使用 ;DOM元素:存儲在$.cache[elem[$.expando]] ;JavaScript對象:obj[$.expando]
- 自定義緩存對象 ;給用戶使用的 ;DOM元素:存儲在$.cache[elem[$.expando]].data ;JavaScript對象:obj[$.expando].data
jQuery的靜態方法含有如下API:
- $.cache ;DOM元素的數據緩存對象,所有DOM元素存儲的數據都會存儲在該對象里
- $.uuid ;唯一id種子,初始值為0,當數據存儲在DOM元素上時用到,元素的$.expando屬性的值等於最新的$.uuid加1
- $.expando ;頁面中每個jQuery副本的唯一標識,只有刷新頁面才會發生變化。格式:jQuery+版本號+隨機數
- $.acceptData(elem) ;判斷DOM元素elem是否可以設置數據,elem是一個DOM節點
- $.hasData(elem) ;判斷elem是否有關聯的數據
- $.data(elem, name, data,pvt) ;設置或返回DOM或JavaScript對象的數據。
·elem是DOM元素或JavaScript對象。
·name是要設置或讀取的數據名,也可以是包含鍵值對的對象。
·data是要設置的數據值,可以是任意數據。
·pvt表示操作的是否為內部數據,預設為false
- $._data(elem, name, data) ;設置、讀取內部數據,內部代碼就一句return jQuery.data( elem, name, data, true )
- $.removeData(elem, name, pvt) ;移除通過$.data()設置的數據,pvt表示是否為內部數據
- $.cleanData(elem) ;移除多個DOM元素的全部數據和事件
jQuery/$ 實例方法(可以通過jQuery實例調用的):
- data(key,value) ;設置/讀取自定義數據
- removeData(key) ;移除匹配元素的自定義數據,key可以是一個字元換或數組,表示屬性或屬性列表
為DOM元素存儲數據時,比較特別,jQuery首先會在該DOM上添加一個名為$.expando的屬性,值是一個唯一的id,等於++$.uuid(jQuery的一個內置屬性),$.uuid是一個整型值,初始值為0。為該DOM添加屬性之後還會把這個id作為屬性添加到全局緩存對象jQuery.cache中,對應的屬性值是一個JavaScript對象,該對象是DOM元素的數據緩存對象
例如:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="http://libs.baidu.com/jquery/1.7.1/jquery.min.js"></script> </head> <body> <p>123</p> <script> var p = document.getElementsByTagName('p')[0]; $.data(p,'age',25,true); //設置內部數據age=25,這是直接定義在數據緩存對象上的。等價於$._data(p,'age',25); $.data(p,'age',23); //設置自定義數據age=23,等價於$.data(p,'age',23,false),這是定義在數據緩存對象的data屬性對象上 console.log($.data(p,undefined,undefined,true)); //輸出: Object { data={ age=23}, age=25} ;獲取數據緩存對象。 console.log($.data(p)); //輸出: Object { age=23} ;獲取自定義緩存對象,其實就是$.data(p,undefined,undefined,true)對象的data屬性 console.log($.cache[p[$.expando]].data === $.data(p)); //輸出true,從這裡可以看出$.data(p)獲取的就是自定義緩存對象,也就是數據緩存對象的data屬性對象 </script> </body> </html>
輸出如下:
源碼分析
對於數據緩存模塊的靜態方法來說,它是以jQuery.extend({})函數直接掛載到jQuery里的,如下:
jQuery.extend({ cache: {}, //DOM元素的數據緩存對象 // Please use with caution uuid: 0, // Unique for each copy of jQuery on the page // Non-digits removed to match rinlinejQuery expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), //頁面中每個jQuery副本的唯一標識 // The following elements throw uncatchable exceptions if you // attempt to add expando properties to them. noData: { //存放了不支持擴展屬性的embed、object、applet元素的節點名稱 "embed": true, // Ban all objects except for Flash (which handle expandos) "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", "applet": true }, hasData: function( elem ) { //判斷一個DOM元素或JavaScript對象是否有與之關聯的數據 elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; //如果是元素節點(有nodeType屬性)則判斷在jQuery.cache中是否有jQuery.expando屬性,否則認為是JavaScript對象,判斷是否有jQuery.expando屬性。 return !!elem && !isEmptyDataObject( elem ); //如果elem存在且含有數據緩存則返回true,isEmptyDataObject是個jQuery內部的工具函數 }, data: function( elem, name, data, pvt /* Internal Use Only */ ) { /*略*/ }, removeData: function( elem, name, pvt /* Internal Use Only */ ) { /*略*/ }, // For internal use only. _data: function( elem, name, data ) { //設置、讀取內部數據,就是調用jQuery.data(),並設置第四個參數為true return jQuery.data( elem, name, data, true ); }, // A method for determining if a DOM node can handle the data expando acceptData: function( elem ) { //判斷參數elem是否可以設置數據,返回true則可以設置,返回false則不可以 if ( elem.nodeName ) { var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; if ( match ) { return !(match === true || elem.getAttribute("classid") !== match); } } return true; } });
我們主要看一下$.data()是怎麼設置數據的,懂了怎麼設置數據,removeData也就懂了,如下:
data: function( elem, name, data, pvt /* Internal Use Only */ ) { //設置、讀取自定義數據、內部數據 if ( !jQuery.acceptData( elem ) ) { //檢查elem元素是否支持設置數據,如果jQuery.acceptData()函數返回false表示不允許設置數據 return; //則直接返回,不繼續操作 } var privateCache, thisCache, ret, //privateCache預設指向數據緩存對象(如果pvt參數未設置或者為false則指向自定義數據),thisCache表示自定義數據緩存對象,如果pvt是true,則privateCache和thisCache都指向數據緩存對象都指向數據緩存對象。ret是讀取時的返回值 internalKey = jQuery.expando, //jQuery.expando頁面中每個jQuery副本的唯一標識,把它賦值給internalKey是為了減少拼寫字數和縮短作用域鏈查找。 getByName = typeof name === "string", //getByName表示name是否為字元串 // We have to handle DOM nodes and JS objects differently because IE6-7 // can't GC object references properly across the DOM-JS boundary isNode = elem.nodeType, //isNode表示elem是否為DOM元素 // Only DOM nodes need the global jQuery cache; JS object data is // attached directly to the object so GC can occur automatically cache = isNode ? jQuery.cache : elem, //如果是DOM元素則存儲在$.cache中,如果是JavaScript對象則存儲在該對象本身 // Only defining an ID for JS objects if its cache already exists allows // the code to shortcut on the same path as a DOM node with no cache id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey, isEvents = name === "events"; // Avoid doing any more work than we need to when trying to get data on an // object that has no data at all if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) { //如果是讀取數據但沒有數據,則返回,避免做不必要的工作,if語句中的符合表達式可以分兩個部分,後一部分是getByName && data === undefined,表示,如果name是字元串且data沒有設置,則說明是在讀數據。 前一部分(!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)表示,如果id不存在說明沒有該屬性,如果cache[id]不存在則說明沒有該數據 return; } /*執行到這裡有兩種情況:1.存儲數據 2.讀取數據且數據存在*/ if ( !id ) { //如果id不存在,則分配一個 // Only DOM nodes need a new unique ID for each element since their data // ends up in the global cache if ( isNode ) { //如果是DOM元素 elem[ internalKey ] = id = ++jQuery.uuid; //jQuery.uuid會自動加1,並附在DOM元素上 } else { id = internalKey; //否則關聯ID就是jQuery.expando } } if ( !cache[ id ] ) { //如果DOM對象或JavaScript對象對應的數據緩存對象不存在則初始化為一個空對象 cache[ id ] = {}; // Avoids exposing jQuery metadata on plain JS objects when the object // is serialized using JSON.stringify if ( !isNode ) { cache[ id ].toJSON = jQuery.noop; } } // An object can be passed to jQuery.data instead of a key/value pair; this gets // shallow copied over onto the existing cache if ( typeof name === "object" || typeof name === "function" ) { //如果name是對象或者函數(函數好像不可以,只能是對象),則批量把參數name中的屬性合併到已有的數據緩存對象上,即批量設置數據 if ( pvt ) { cache[ id ] = jQuery.extend( cache[ id ], name ); } else { cache[ id ].data = jQuery.extend( cache[ id ].data, name ); } } privateCache = thisCache = cache[ id ]; //設置privateCache和thisCache都指向數據緩存對象cache[ id ] // jQuery data() is stored in a separate object inside the object's internal data // cache in order to avoid key collisions between internal data and user-defined // data. if ( !pvt ) { //如果參數pvt是false或者未設置,則設置thisCache指向自定義數據, if ( !thisCache.data ) { //如果數據緩存對象thisCache.data不存在則先將其初始化為空對象。 thisCache.data = {}; } thisCache = thisCache.data; } if ( data !== undefined ) { //如果data不是undefined,則把參數data設置到屬性name上,這裡統一把參數name轉換成了駝峰式,這樣在讀取的時候不管是連字元串還是駝峰式就都不會出錯。 thisCache[ jQuery.camelCase( name ) ] = data; } // Users should not attempt to inspect the internal events object using jQuery.data, // it is undocumented and subject to change. But does anyone listen? No. if ( isEvents && !thisCache[ name ] ) { return privateCache.events; } // Check for both converted-to-camel and non-converted data property names // If a data property was specified if ( getByName ) { //如果參數name是字元串,則讀取單個數據 // First Try to find as-is property data ret = thisCache[ name ]; //先嘗試讀取參數name對應的數據 // Test for null|undefined property data if ( ret == null ) { //如果沒有讀取到則把參數name轉換為駝峰式再次嘗試讀取 // Try to find the camelCased property ret = thisCache[ jQuery.camelCase( name ) ]; } } else { ret = thisCache; //如果參數2不是字元串,則返回數據緩存對象。 } return ret; //最後返回ret },
這樣就完成數據的設置的,對於jQuery實例上的方法如下:
jQuery.fn.extend({ data: function( key, value ) { //設置、讀取自定義數據,解析html5屬性data- key是要設置或讀取的數據名,或者是含有鍵值對的對象,value是要設置的數據值,可以是任意類型 var parts, attr, name, data = null; if ( typeof key === "undefined" ) { //如果未傳入參數,即參數格式是.data(),則獲取第一個匹配元素關聯的數據緩存對象(即獲得全部數據) if ( this.length ) { //如果該jQuery對象有匹配的元素 data = jQuery.data( this[0] ); //獲取第一個元素的數據緩存對象 if ( this[0].nodeType === 1 && !jQuery._data( this[0], "parsedAttrs" ) ) { //這裡是解析html5里的data-屬性的,可以先略過 attr = this[0].attributes; for ( var i = 0, l = attr.length; i < l; i++ ) { name = attr[i].name; if ( name.indexOf( "data-" ) === 0 ) { name = jQuery.camelCase( name.substring(5) ); dataAttr( this[0], name, data[ name ] ); } } jQuery._data( this[0], "parsedAttrs", true ); //返回第一個匹配元素關聯的自定義數據緩存對象。如果沒有匹配元素則會返回null } } return data; } else if ( typeof key === "object" ) { //如果key是一個對象,則為每個元素對象調用方法$.data(this,key)批量設置數據 return this.each(function() { jQuery.data( this, key ); }); } parts = key.split("."); parts[1] = parts[1] ? "." + parts[1] : ""; //取出命名空間,比如$(this).data('a.b',123);則parts[1]是.b if ( value === undefined ) { //如果傳入的格式是.data(key),則認為是讀取單個數據 data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); //觸發自定義事件getData,並把事件監聽函數的返回值賦值給變數data // Try to fetch any internally stored data first if ( data === undefined && this.length ) { //如果事件監聽函數沒有返回值,才會嘗試從自定義數據緩存對象中讀取 data = jQuery.data( this[0], key ); data = dataAttr( this[0], key, data ); } return data === undefined && parts[1] ? //如果從getData()事件監聽函數或自定義數據緩存對象或HTML5屬性data-中取到了數據,則返回數據;如果沒有取到數據,但是指定了命名空間,則去掉命名空間再次嘗試讀取。 this.data( parts[0] ) : data; } else { //如果傳入了參數key和value,即參數格式是:.data(key,value),則為每個匹配元素設置任意類型的數據,並觸發自定義事件setData()和changeData()。 return this.each(function() { var self = jQuery( this ), args = [ parts[0], value ]; self.triggerHandler( "setData" + parts[1] + "!", args ); //觸發自定義事件setData,感嘆號表示只執行沒有命名控制項的事件監聽函數 jQuery.data( this, key, value ); //調用$.data()方法為任意匹配元素設置任意類型的數據 self.triggerHandler( "changeData" + parts[1] + "!", args ); //觸發自定義事件changeData }); } }, /*...*/ })
設置數據緩存就是這樣的,理解了設置數據緩存,移除就很好理解了。