【原创】jQuery1.8.2源码解析之jQuery.data
数据缓存,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 }