jQuery的数据缓存系统
数据缓存系统最早在jQuery1.2版本开始引入,它是用于关联操作对象和与之相关的数据的一种机制。在DOM中,我们通常操作的数据有3种:元素节点、文档对象与window对象。1.2之前的jQuery事件系统照搬Dean Edwards的addEvent.js,然而addEvent在实现有个缺憾,它将事件的回调都放在了EventTarget之上,这会引起循环引用,IE下就会有内存泄露,如果EventTarget是window对象,又会引发全局污染。
数据缓存系统,除了规避以上这两个风险外,我们还可以有效地保存不同方法产生的中间变量,而这些变量会对另一个模块的方法有用,解耦方法间的依赖。对于jQuery来说,它的事件克隆乃至后来的队列实现都离不开缓存系统。
数据缓存产生的需求无外乎就是描述一个DOM节点的相关数据,以下有几种方案:
使用attributes
<div id="demo" node-type="test"></div>
var demo = document.getElementById('demo');
console.log(demo.getAttribute('node-type'));
使用html5的dataset
<div id="demo" data-type="test"></div>
var demo = document.getElementById('demo');
console.log(demo.dataset.type);
DOM扩展
<div id="demo"></div>
var demo = document.getElementById('demo');
demo['node-type'] = 'test';
console.log(demo['node-type']);
以上解决方案虽然解决了问题,但存在局限性:
- 方案1、2只能保存字符串的数据,同时曝露了数据,并且在DOM上挂载了无谓的属性,浏览器仍然会尝试解析这些属性
- 方案3污染DOM,同时存在循环引用的风险
jQuery的数据缓存系统解决了上述问题 jQuery建立一个仓库,所有的数据放置于此,需要挂载数据的对象分配一个key,通过这个key到这个仓库中获取值
实现
jQuery在创建的时候,生成一个属性expando,每个需要挂载的对象中存在一个属性,即expando的值,该属性的值+随机数就是仓库中所对应的key
jQuery.extend({
expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" )
})
//jQuery + (版本号 + 随机数)_去除非数字
Data对象
jQuery中的data有两种,一种是用户可以操作的data_user,另一种是jQuery事件系统之类的内部实现使用的私有数据data_priv,但他们都是同一个Data原型函数实例化出来的。
初始化
function Data(){
//初始化时,将该对象的cache置为{0:{}}
Object.defineProperty( this.cache = {}, 0, {
get: function(){
return {};
}
});
//jQuery2110499953880906105040.42317264270968735
this.expando = jQuery.expando + Math.random();
}
//初始化Data的唯一标示
Data.uid = 1;
//Data可以接受的数据类型
Data.accepts = jQuery.acceptData;
Data可以接受的数据类型
jQuery.acceptData = function( owner ){
//element、document、object
return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );
}
Data原型
- 获取key
- get/set
- access 根据参数选择get或者set
- remove 移除数据
- hasData 检查是否有数据
- discard 删除该储存空间
1、key
- this.expando每次实例化生成唯一
- unlock为Data.uid自增
- this.expando为key,unlock为value,添加到owner中;unlock作为key在data_priv/data_user中找到对应的value
Data.prototype.key = function( owner ){
//非Data允许的对象直接返回0
if( !Data.accepts( owner ) ){
return 0;
}
var descriptor = {},
unlock = owner[ this.expando ];
//是否已有cache
if( !unlock ){
unlock = Data.uid++;
try{
//防止此属性被遍历和改写
descriptor[ this.expando ] = { value: unlock };
Object.defineProperties( owner, descriptor );
}catch (e) {
descriptor[ this.expando ] = unlock;
jQuery.extend( owner, descriptor );
}
}
//初始化cache
if( !this.cache[ unlock ] ){
this.cache[ unlock ] = {};
}
return unlock;
}
2、set
- 通过自身的key方法找到owner中的key值,进而获取到cache,对cache进行赋值操作
Data.prototype.set = function( owner, data, value ){
var prop,
unlock = this.key( owner ),
cache = this.cache[ unlock ];
//data为字符串
if( typeof data === 'string' ){
cache[ data ] = value;
}else{
//cache没有可遍历的值
if( jQuery.isEmptyObject( cache ) ){
jQuery.extend( this.cache[ unlock ], data );
}else{
//此处采用的是浅复制
for( prop in data ){
cache[ prop ] = data[ prop ];
}
}
}
return cache;
}
3、get
- 如果没有key,就获取所有cache,否则获取对应的cache[key]
Data.prototype.get = function( owner, key ){
var cache = this.cache[ this.key( owner ) ];
return key === undefined ? cache : cache[ key ];
}
4、access
- access( owner )或者access( owner, key )->get方法,否则->set方法
Data.prototype.access = function( owner, key, value ){
var stored;
if( key === undefined || ( ( key && typeof key === 'string') && value === undefined ) ){
stored = this.get( owner, key );
return stored === undefined ? stored : this.get( owner, jQuery.camelCase( key ) );
}
this.set( owner, key, value );
return value !== undefined ? value : key;
}
5、remove
- 如果key为空,此时将cache置为空对象
- key为数组时,将数组值用jQuery.camelCase方法执行一遍追加到原数组中,以此来兼容带有'-'的情况;key为字符串时直接使用该方法
- 最后遍历执行delete操作
- camelCase将'-xxx'全部转化为'XXX'
Data.prototype.remove = function( owner, key ){
var i, name, camel,
unlock = this.key( owner ),
cache = this.cache[ unlock ];
if( key === undefined ){
this.cache[ unlock ] = {};
} else {
if( jQuery.isArray( key ) ){
name = key.concat( key.map( jQuery.camelCase ) );
} else {
camel = jQuery.camelCase( key );
if( key in cache ){
name = [ key, camel ];
} else {
name = camel;
//此处兼容了空格分隔key的情况
name = name in cache ? [ name ] : ( name.match( rnotwhite ) || [] );
}
i = name.length;
while( i-- ){
delete cache[ name[ i ] ];
}
}
}
}
6、hasData
- 使用jQuery.isEmptyObject方法判断,即是否有可枚举的属性
Data.prototype.hasData = function( owner ){
return !jQuery.isEmptyObject( this.cache[ owner[ this.expando ] ] || {});
}
7、discard
- 清空该owner的储存空间
Data.prototype.discard = function( owner ){
if( owner[ this.expando ] ){
delete this.cache[ owner[ this.expando ] ];
}
}
$('').data()与$.data区别
看下面代码
<div id="test"></div>
var node1 = $('#test');
var node2 = $('#test');
//===========$('').data()============
node1.data('a',1);
node2.data('a',2);
node1.data('a');//2
node2.data('a');//2
//===========$.data()==============
$.data(node1,'b',1);
$.data(node2,'b',2);
$.data(node1,'b');//1
$.data(node2,'b');//2
$('').data()会覆盖,$.data()则不会。分析下源代码
jQuery.extend({
data: function( elem, name, data ){
//直接调用Data.prototype.access,传入的是整个elem的jQuery对象
return data_user.access( elem, name, data );
}
})
jQuery.fn.extend({
data: function( key,value ){
return access( this, function( value ){
//此处是遍历dom每个节点,将缓存数据与dom节点关联
this.each(function(){
})
})
}
})
- $.data()针对的是每一个elem的jQuery对象,由于node1和node2是不同的jQuery对象,所以自然有不同的{key:value}缓存数据
- $('').data()针对的是具体的dom节点,node1和node2都是指向#container这个dom,所以引用的是同一个{key:value}缓存数据
jQuery自身扩展
这里都是调用的Data原型上的方法
jQuery.extend({
hasData: function( elem ){
return data_user.hasData( elem ) || data_priv.hasData( elem );
},
data: function( elem, name, data ){
return data_user.access( elem, name, data );
},
removeData: function( elem, name ){
return data_user.remove( elem, name );
},
_data: function( elem, name, data ){
return data_priv.access( elem, name, data );
},
_removeData: function( elem, name ){
data_priv.remove( elem, name );
}
})
jQuery对象API扩展
$('').data()
- 获取data全部值时,首先获取data_user中的数据,然后将html5中的data-属性的值添加至data_user中,其中将data_priv中的hasDataAttrs作为标志位,如果为true,则不再遍历其data-属性
jQuery.fn.extend({
data: function(key, value){
var i, name, data,
elem = this[0],
attrs = elem && elem.attributes;
if( key === undefined ){
if( this.length ){
data = data_user.get( elem );
//将elem节点中的data-xxx 属性放入data_user
if( elem.nodeType === 1 && !data_priv.get( elem, "hasDataAttrs" ) ){
i = attrs.length;
while( i-- ){
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;
}
//值为对象时
if( typeof key === "object" ){
return this.each(function(){
data_user.set( this, key );
})
}
return access( this, function( value ){
//access在jQuery中是一个可以set/get的多功能函数,此处会使用this.each遍历jQuery对象,set/get数据
}, null, value, arguments.length > 1, null, true );
}
})
dataAttr函数
- 将elem节点中的data-xxx属性值放入data_user中,提升效率
var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,
rmultiDash = /([A-Z])/g;
function dataAttr( elem, key, data ){
if( data === undefined && elem.dataType === 1 ){
name = "data-" key.replace( rmultiDash, "-$1" ).toLowerCase();
data = elem.getAttribute(name);
if(typeof data === "string"){
try{
data = data === "true" ? true ://转化true
data === "false" ? false ://转化false
data === "null" ? null ://转化null
+data + "" === data ? +data ://转化数字
rbrace.test( data ) ? jQuery.parseJSON( data ) ://转化json字符串
data;
} catch ( e ) {}
data_user.set( elem, key, data );
}else{
data = undefined;
}
}
return data;
}
mark