數據緩存,jQuery現在支持兩種:
1. dom元素,數據存儲在jQuery.cache中。
2.普通js對象,數據存儲在該對象中。
以下是源代碼:
1 var rbrace = /^(?:\{.*\}|\[.*\])$/, 2 rmultiDash = /([A-Z])/g; 3 4 // 首先是對jQuery對象自身的擴展 5 jQuery.extend({ 6 // 即jQuery.cache,負責存儲dom元素的緩存數據 7 cache: {}, 8 9 // removeData時,緩存的數據被清除,返回的當時對應的id,以便再利用 10 deletedIds: [], 11 12 // Please use with caution 13 // 將數據存儲到jQuery.cache中時,需要唯一id,用它來維護 14 uuid: 0, 15 16 // Unique for each copy of jQuery on the page 17 // Non-digits removed to match rinlinejQuery 18 // 內部key(隨即生成),之后會作為key添加到dom的屬性集中,而key對應的value則是該dom對應的緩存對象 19 expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), 20 21 // The following elements throw uncatchable exceptions if you 22 // attempt to add expando properties to them. 23 // 不能添加expando屬性的dom 24 // classid為'clsid:D27CDB6E-AE6D-11cf-96B8-444553540000'的object是可以的,比較特殊吧 25 noData: { 26 "embed": true, 27 // Ban all objects except for Flash (which handle expandos) 28 "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", 29 "applet": true 30 }, 31 32 hasData: function( elem ) { 33 elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; 34 return !!elem && !isEmptyDataObject( elem ); 35 }, 36 37 data: function( elem, name, data, pvt /* Internal Use Only */ ) { 38 if ( !jQuery.acceptData( elem ) ) { 39 return; 40 } 41 42 var thisCache, ret, 43 internalKey = jQuery.expando, 44 getByName = typeof name === "string", 45 46 // We have to handle DOM nodes and JS objects differently because IE6-7 47 // can't GC object references properly across the DOM-JS boundary 48 // 也就是說dom元素和普通js對象要進行不同的處理 49 // 原因好像是是垃圾回收不能正確處理添加到dom元素的引用 50 isNode = elem.nodeType, 51 52 // Only DOM nodes need the global jQuery cache; JS object data is 53 // attached directly to the object so GC can occur automatically 54 // dom元素我們借用全局jQuery.cache來存儲數據 55 // 普通的js對象則直接將數據存儲到對象中,垃圾回收可以自動處理 56 cache = isNode ? jQuery.cache : elem, 57 58 // Only defining an ID for JS objects if its cache already exists allows 59 // the code to shortcut on the same path as a DOM node with no cache 60 // 1. 如果是dom元素,返回dom元素expando對應的id(值可能為undefined) 61 // 2. 如果是普通js對象,分兩種情況: 62 // 2.1 如果js對象存在expando對應的值,即代表有緩存數據,則立即返回expando作為id 63 // 2.2 如果沒有對應值,則代表沒有緩存數據,此時返回undefined 64 // 也就是說如果id不為空,那么肯定是有存儲數據過的 65 id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; 66 67 // Avoid doing any more work than we need to when trying to get data on an 68 // object that has no data at all 69 // 如果id不存在(表示不存在緩存) 70 // 或者id存在,但是緩存為空 71 // 又或者此時數據是私有的(pvt為true,僅為內部使用,此時只操控到cache[id]這一層) 72 // 又或者數據不是私有的,但是對應的數據(data)為空 73 // 以上條件之一成立后, 74 // 再加上,getByName && data === undefined(表示是取數據)這個條件,直接return就可以了,因為沒有數據取 75 // 如果getByName為false,那么將初始化緩存對象(也為后來可能的name為object或者function時,extend做准備) 76 if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) { 77 return; 78 } 79 // 如果id為空,表示dom元素或者普通js對象沒有緩存 80 if ( !id ) { 81 // Only DOM nodes need a new unique ID for each element since their data 82 // ends up in the global cache 83 // dom元素需要唯一id,因為它的數據將存在全局的jQuery.cache中 84 if ( isNode ) { 85 elem[ internalKey ] = id = jQuery.deletedIds.pop() || ++jQuery.uuid; 86 } else { 87 // 普通js對象的id都是expando 88 id = internalKey; 89 } 90 } 91 92 // 如果緩存為空,則之前沒有存儲過數據,此時需要進行必要的初始化 93 if ( !cache[ id ] ) { 94 // 創建緩存對象(理解為一個存放鍵值對的集合) 95 cache[ id ] = {}; 96 97 // Avoids exposing jQuery metadata on plain JS objects when the object 98 // is serialized using JSON.stringify 99 // 普通js對象需要在它的緩存對象中添加toJSON方法,其中jQuery.noop只是有個空函數,什么都不做 100 // 這里的目的是:在使用JSON.stringify(elem)序列化該js對象時,使它的緩存對象不參與序列化(空函數返回空) 101 // 而dom元素是無法使用JSON.stringify(dom)的,會報錯Converting circular structure to JSON 102 if ( !isNode ) { 103 cache[ id ].toJSON = jQuery.noop; 104 } 105 } 106 107 // An object can be passed to jQuery.data instead of a key/value pair; this gets 108 // shallow copied over onto the existing cache 109 // 就是說在適用jQuery.data()緩存數據時,除了傳遞key/value鍵值對外,還可以傳遞一個對象,或者一個函數(返回一個對象) 110 // 這樣的結果是:傳遞的對象將會被extend到緩存中去 111 if ( typeof name === "object" || typeof name === "function" ) { 112 if ( pvt ) { 113 // 私有數據,這里我明白了大概pvt的用處 114 // cache[id].data 對象是用來存儲用戶自定義數據 115 // cache[id] 則存儲的是系統內部數據,比如之前說的toJSON 116 // pvt不為空,則處理用戶自定義數據,定位到cache[id].data這一層 117 // pvt為空,則處理系統內部數據,定位到cache[id]這一層 118 cache[ id ] = jQuery.extend( cache[ id ], name ); 119 } else { 120 cache[ id ].data = jQuery.extend( cache[ id ].data, name ); 121 } 122 } 123 124 thisCache = cache[ id ]; 125 126 // jQuery data() is stored in a separate object inside the object's internal data 127 // cache in order to avoid key collisions between internal data and user-defined 128 // data. 129 // 就是說為了防止系統內部數據和用戶自定義數據的key發生沖突,才將用戶數據包在thisCache.data中, 130 // 系統內部數據就是thisCache中 131 if ( !pvt ) { 132 if ( !thisCache.data ) { 133 thisCache.data = {}; 134 } 135 136 //此時thisCache指向真正的數據緩存(集合) 137 thisCache = thisCache.data; 138 } 139 140 // 如果data不為undefined,則表示這是在設置數據(set),那么進行負值緩存操作 141 // jQuery.camelCase( name )將name駝峰化 142 if ( data !== undefined ) { 143 thisCache[ jQuery.camelCase( name ) ] = data; 144 } 145 146 // Check for both converted-to-camel and non-converted data property names 147 // If a data property was specified 148 // 返回指定name的數據,包括取數據和設置數據,都會返回。 149 if ( getByName ) { 150 151 // First Try to find as-is property data 152 // 首先嘗試取數據 153 ret = thisCache[ name ]; 154 155 // Test for null|undefined property data 156 // 如果ret為null或者undefined,則嘗試將name駝峰化再嘗試取數據(因為有可能之前name就被駝峰化) 157 if ( ret == null ) { 158 // Try to find the camelCased property 159 ret = thisCache[ jQuery.camelCase( name ) ]; 160 } 161 } else { 162 // 沒有指定name,則返回整個緩存對象 163 ret = thisCache; 164 } 165 //返回數據 166 return ret; 167 }, 168 169 removeData: function( elem, name, pvt /* Internal Use Only */ ) { 170 if ( !jQuery.acceptData( elem ) ) { 171 return; 172 } 173 174 var thisCache, i, l, 175 176 isNode = elem.nodeType, 177 178 // See jQuery.data for more information 179 cache = isNode ? jQuery.cache : elem, 180 id = isNode ? elem[ jQuery.expando ] : jQuery.expando; 181 182 // If there is already no cache entry for this object, there is no 183 // purpose in continuing 184 // 沒有緩存,直接退出 185 if ( !cache[ id ] ) { 186 return; 187 } 188 189 // 如果有傳入name,那么刪除指定name對應的數據 190 // 否則刪除所有緩存,后面的代碼有這一步處理 191 if ( name ) { 192 // 這里還是一樣,通過內部pvt指定緩存層級,用戶自定義數據層和系統內部數據層 193 thisCache = pvt ? cache[ id ] : cache[ id ].data; 194 195 if ( thisCache ) { 196 197 // Support array or space separated string names for data keys 198 // 支持單個的key 199 // 數組,多個key,如:[key1, key2, key3, ...] 200 // 字符串,多個key,用空格隔開,如:'key1 key2 key3 ...' 201 202 //不是數組的情況,最終轉換為數組形式 203 if ( !jQuery.isArray( name ) ) { 204 205 // try the string as a key before any manipulation 206 // 首先直接查找 207 if ( name in thisCache ) { 208 name = [ name ]; //轉換成數組形式,便於后面統一操作 209 } else { 210 211 // split the camel cased version by spaces unless a key with the spaces exists 212 name = jQuery.camelCase( name ); 213 // 駝峰化后再查找 214 if ( name in thisCache ) { 215 name = [ name ]; 216 // 用字符串轉換為數組 217 } else { 218 name = name.split(" "); 219 } 220 } 221 } 222 // 統一用數組進行刪除操作 223 // 有一個疑問就是,如果數組元素沒有被駝峰化,應該會出錯?! 224 for ( i = 0, l = name.length; i < l; i++ ) { 225 // delete thisCache[ jQuery.camelCase(name[i]) ]; 226 delete thisCache[ name[i] ]; 227 } 228 229 // If there is no data left in the cache, we want to continue 230 // and let the cache object itself get destroyed 231 // 如果緩存不為空,則退出 232 // 否則,需要進行下一步的清理工作,因為此時緩存為空了嘛 233 if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { 234 return; 235 } 236 } 237 } 238 239 // See jQuery.data for more information 240 if ( !pvt ) { 241 // 去除data屬性 242 delete cache[ id ].data; 243 244 // Don't destroy the parent cache unless the internal data object 245 // had been the only thing left in it 246 // 當data處理過后需要檢測cache[id],因為此時cache[id]可能處於空的狀態(這里的所謂的空在isEmptyDataObject有說明) 247 if ( !isEmptyDataObject( cache[ id ] ) ) { 248 return; 249 } 250 } 251 252 // Destroy the cache 253 // 如果是dom元素,除了jQuery.cache清理完畢后,還要處理dom元素自身,因為綁定了一個id嘛 254 if ( isNode ) { 255 jQuery.cleanData( [ elem ], true ); 256 257 // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) 258 } else if ( jQuery.support.deleteExpando || cache != cache.window ) { 259 delete cache[ id ]; 260 261 // When all else fails, null 262 } else { 263 cache[ id ] = null; 264 } 265 }, 266 267 // For internal use only. 268 // 內部適用,這里設置pvt為true,返回內部數據,定位到cache[id]這一層 269 _data: function( elem, name, data ) { 270 return jQuery.data( elem, name, data, true ); 271 }, 272 273 // A method for determining if a DOM node can handle the data expando 274 // 根據上面的jQuery.noData屬性判斷dom該元素是否可以添加expando屬性(即dom是否允許添加數據) 275 // 其中,classid為'clsid:D27CDB6E-AE6D-11cf-96B8-444553540000'的object是可以的,比較特殊吧 276 acceptData: function( elem ) { 277 var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; 278 279 // nodes accept data unless otherwise specified; rejection can be conditional 280 return !noData || noData !== true && elem.getAttribute("classid") === noData; 281 } 282 }); 283 284 // 接下來是對jQuery對象($(selector))的擴展 285 jQuery.fn.extend({ 286 data: function( key, value ) { 287 var parts, part, attr, name, l, 288 elem = this[0], 289 i = 0, 290 data = null; 291 292 // Gets all values 293 // 如果為連key的值都未指定,那么返回的所有數據 294 // 如:$dom.data(); 295 if ( key === undefined ) { 296 if ( this.length ) { 297 // 先從jquery緩存中取出所有數據 298 data = jQuery.data( elem ); 299 300 // 對於元素節點而言,數據可以來自兩個地方: 301 // 1. jQuery.cache緩存中,之前手動存進去的,如:$dom.data('data1', value1); 302 // 2. 來自html標簽的以data-開頭的屬性,之后該屬性的數據也會被存儲到jQuery.cache緩存中, 303 // (屬性名采用駝峰的形式)避免每次都要去html標簽里去匹配並取值 304 // 如:<div data-data-first="{a:1,b:2}" data-data-second="hello">hello world</div> 305 // 當使用$dom.data()時,會獲取到: 306 // { 307 // dataFirst : { 308 // a : 1, 309 // b : 2 310 // } 311 // dataSecond : 'hello world' 312 // } 313 314 // 通過緩存中的內部屬性parsedAttrs,分析html標簽屬性所帶的數據是否被解析過(即存到jQuery過緩存中) 315 // 解析過了,那么這里就沒必要再解析一遍了,上面一步就已經取到數據了 316 if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { 317 attr = elem.attributes; 318 // 遍歷dom節點的屬性列表 319 for ( l = attr.length; i < l; i++ ) { 320 name = attr[i].name; 321 // 對於屬性名以data-開頭的屬性進行取值存儲操作 322 if ( name.indexOf( "data-" ) === 0 ) { 323 // 首先name去除data-,並將剩余的字符駝峰化 324 name = jQuery.camelCase( name.substring(5) ); 325 326 dataAttr( elem, name, data[ name ] ); 327 } 328 } 329 // 標記html標簽上的數據已經解析過 330 jQuery._data( elem, "parsedAttrs", true ); 331 } 332 } 333 334 return data; 335 } 336 337 // Sets multiple values 338 // 傳遞對象(鍵值對)作為data緩存,此時是對jQuery對象列表進行each操作 339 if ( typeof key === "object" ) { 340 return this.each(function() { 341 jQuery.data( this, key ); 342 }); 343 } 344 345 parts = key.split( ".", 2 ); 346 parts[1] = parts[1] ? "." + parts[1] : ""; 347 part = parts[1] + "!"; 348 349 return jQuery.access( this, function( value ) { 350 351 if ( value === undefined ) { 352 data = this.triggerHandler( "getData" + part, [ parts[0] ] ); 353 354 // Try to fetch any internally stored data first 355 if ( data === undefined && elem ) { 356 // 首先從jQuery緩存中獲取 357 data = jQuery.data( elem, key ); 358 // 再從html標簽里面獲取(可見標簽數據的優先級高) 359 data = dataAttr( elem, key, data ); 360 } 361 362 return data === undefined && parts[1] ? 363 this.data( parts[0] ) : 364 data; 365 } 366 367 parts[1] = value; 368 this.each(function() { 369 var self = jQuery( this ); 370 371 self.triggerHandler( "setData" + part, parts ); 372 jQuery.data( this, key, value ); 373 self.triggerHandler( "changeData" + part, parts ); 374 }); 375 }, null, value, arguments.length > 1, null, false ); 376 }, 377 378 removeData: function( key ) { 379 return this.each(function() { 380 jQuery.removeData( this, key ); 381 }); 382 } 383 }); 384 385 function dataAttr( elem, key, data ) { 386 // If nothing was found internally, try to fetch any 387 // data from the HTML5 data-* attribute 388 // 如果data為空,且elem為元素節點,那么從標簽的數據屬性取數據(遵循html5) 389 if ( data === undefined && elem.nodeType === 1 ) { 390 // 將駝峰化轉換成'-'連接的小寫字符串 391 var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); 392 // 獲取dom上的對應屬性 393 data = elem.getAttribute( name ); 394 // 如果該屬性存在,此時data為字符串,下面將進行根據數據類型進行數據的格式化 395 if ( typeof data === "string" ) { 396 try { 397 // 布爾型 398 data = data === "true" ? true : 399 data === "false" ? false : 400 // null 401 data === "null" ? null : 402 // Only convert to a number if it doesn't change the string 403 // +data用來測試類型是否為數字 404 +data + "" === data ? +data : 405 // 對象和數組 406 rbrace.test( data ) ? jQuery.parseJSON( data ) : 407 data; 408 } catch( e ) {} 409 410 // Make sure we set the data so it isn't changed later 411 // 將格式化的數據存在jQuery.cache緩存。 412 //(注意這里存的是jQuery.cache中,也就是說之前通過$dom.data()獲取的對象,因為是引用,所以此時也是有值的) 413 jQuery.data( elem, key, data ); 414 415 } else { 416 // 如果該屬性不存在,此時data為null,將其轉換為undefined 417 data = undefined; 418 } 419 } 420 // 返回標簽屬性數據 421 return data; 422 } 423 424 // checks a cache object for emptiness 425 // 內部使用,用於檢測cache[id]這一層是否為空 426 // 其中,toJSON 不參與檢測,也就是說只有它存在時,也算是空 427 // 其中,data 參與檢測,如果data不為空,整個cache[id]則不為空 428 function isEmptyDataObject( obj ) { 429 var name; 430 for ( name in obj ) { 431 432 // if the public data object is empty, the private is still empty 433 // 對於data屬性,需要額外判斷data里面是否有數據 434 // 如果沒有,則data為空,那么跳過data,繼續檢測 435 // 否則將在下面的返回false,表示不為空 436 if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { 437 continue; 438 } 439 // toJSON 這個方法可以認為是是內置的,可以忽略它。 440 // 如果不是toJSON 一律返回false,表示還有其他數據 441 if ( name !== "toJSON" ) { 442 return false; 443 } 444 } 445 446 return true; 447 }
