如果与java语言世界对比的话,jQuery Data模块和jQuery Queue模块类似java的集合框架。jQuery Data模块的功能简单来说就是"在某个元素"上对缓存数据"做增删改查的CRUD操作"。"某个元素"可能是原生的dom对象,也可能是js内核对象,jQuery Data模块区别对待它们。按照jQuery通用编程风格,一般对数据的写和读是设计到同一个方法的,更新也属于写操作类型,这样就只有两个方法了--data()和removeData(),当然一般还会设计一个判断数据是否存在的hasData()方法。另外,jQuery Data模块考虑到了私有数据存储的需求并从逻辑(而不是语法)上实现了一个方案_data(),对于某些特殊的不支持数据存储的dom元素,jQuery Data给出了判断方法acceptData()。
一 jQuery Data模块的API
先看jQuery Data模块的几种常见使用方法。
1 写数据
(1) 写单条数据
jQuery.prototype实例方法: $("x").data("p1", "v1");
jQuery静态方法:jQuery.data(elem, "p1", "v1");
(2) 写多条数据
jQuery.prototype实例方法: $("x").data({"p1":"v1","p2":"v2","p3":"v3"});
jQuery静态方法:jQuery.data(elem, {"p1":"v1","p2":"v2","p3":"v3"});
2 读数据
(1) 读单条数据
jQuery.prototype实例方法: $("x").data("p1");
jQuery静态方法:jQuery.data(elem, "p1");
(2) 读全部数据
jQuery.prototype实例方法: $("x").data();
jQuery静态方法:jQuery.data(elem);
3 删数据
jQuery.prototype实例方法: $("x").removeData("p1");
jQuery静态方法:jQuery.removeData(elem, "p1");
二 设计方案
jQuery Data模块设计思路相对其他模块而言是比较简单的,这里概略总结一下。
1 对于原生dom对象
首先排除那些不能接受data缓存的dom元素类型,这些类型被标记于noData对象中,只要dom对象节点名称和对应属性符合noData集合规则的就是不能接受data缓存的dom类型:'var match = jQuery.noData[ elem.nodeName.toLowerCase() ];',对应acceptData()方法将返回false。
对于那些能接受data缓存的dom对象,每个对象的数据缓存在全局缓存对象jQuery.cache中,而不是dom对象本身中!这一点是关键点,在全局缓存对象jQuery.cache中为每一个dom对象开辟一个缓存对象缓存该dom的data数据,由此引出一个问题是:如何关联上不同的dom对象到全局缓存对象jQuery.cache的不同缓存对象上?jQuery的方案是给每一个dom对象添加一个属性,该属性名称为'jQuery.expando'变量的值,这个值由三个部分组成:'jQuery'、'version'、'random数字值',其规则是'expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ),',这个变量的值这样设计是为了最大化保证不与用户自定义变量值雷同而引起冲突,所有能缓存data的dom对象的该属性名称都是一致的;但是每个dom对象该属性的值却是唯一的,类似于关系型数据库表的主键规则,由一个全局uuid唯一生成'elem[ jQuery.expando ] = id = ++jQuery.uuid;',这个值是唯一的且递增的整数值。
dom对象属性'jQuery.expando'的值'jQuery.uuid'将作为全局缓存对象的key,全局缓存对象的value就是独立的一个缓存对象thisCache,存储对应dom对象需要缓存的所有data。这样根据每个不同的dom对象的属性值就能在全局缓存对象jQuery.cache中唯一对应上一个独立缓存对象,对dom对象缓存data的CRUD操作说白了就是对这个独立缓存对象thisCache的CRUD操作。
2 对于js内核对象
js内核对象的缓存就没那么绕了,因为js内核对象本身就可以开辟一个属性作为独立缓存对象thisCache,这个属性的名称也简单,就是'jQuery.expando'的值,所有js内核对象的缓存属性的名称都是一致的。至于dom对象为什么不能向js内核对象一样直接存储缓存数据,源码中给出的解释原因是涉及到浏览器的内存回收机制--如果直接在dom对象上添加独立缓存对象,那么这个dom对象由于引用了独立缓存对象,垃圾回收机制根据'引用计数法'将无法判定该dom对象是可以被回收的,这样长时间运行积累下来可能导致OOM(这个理解可能不准确)。
三 主要方法的源码分析
1 jQuery.data( elem, name, data, pvt /* Internal Use Only */ )方法源码
由于data一个方法就承担数据操作的C/R/U三种类型,所以其参数调用和内部实现逻辑比较多样化。
2 jQuery.removeData( elem, name, pvt /* Internal Use Only */ )方法源码
removeData方法只承担数据操作的D类型,相对功能单一,方法内部流程类似,不同的是当子级独立缓存对象内部已经没有缓存的数据了时需要delete掉该独立缓存对象本身以及dom对象的对应属性。
四 内部私有数据/二级缓存方案
如果调用方希望缓存的data不是外部可访问的,jQuery Data对于这些数据的存储地点是单独维护的,位于子级独立缓存对象的内部名称同样为'jQuery.expando'的属性值对象中,相当于是一个二级缓存方案,jQuery Data模块的调用者知道怎么访问和存入数据,其他用户则不关心这块数据,这是_data()方法通过调用'jQuery.data( elem, name, data, true );'方法并传入第四个参数pvt为true来实现的,jQuery.data()方法内部有独立的逻辑判断并处理私有数据。
jQuery Data模块的实现是比较通用的,即它支持在dom对象/内核对象上存储"任意类型"的缓存数据,这些缓存数据将用于上层模块,按照上层模块的意图和需求,这些缓存数据可以是任意类型的。比如对jQuery Event模块的需求而言,在选择集dom元素上存入的缓存数据是"可执行函数集合",这个时候jQuery Data模块中缓存的数据对象的作用就非常类似jQuery Deffered模块中的回调函数存储对象了,也即它扮演的是"弹夹"的角色,按照每个dom元素事件类型分别存储一个事件回调函数队列,就像不同口径不同规格的子弹被各自的弹夹存放一样。对于其他模块而言,不一定只存储函数数据,完全可以根据模块设计者的需求存储其他类型数据。后续分析jQuery Event模块时将深入该主题。
一 jQuery Data模块的API
先看jQuery Data模块的几种常见使用方法。
1 写数据
(1) 写单条数据
jQuery.prototype实例方法: $("x").data("p1", "v1");
jQuery静态方法:jQuery.data(elem, "p1", "v1");
(2) 写多条数据
jQuery.prototype实例方法: $("x").data({"p1":"v1","p2":"v2","p3":"v3"});
jQuery静态方法:jQuery.data(elem, {"p1":"v1","p2":"v2","p3":"v3"});
2 读数据
(1) 读单条数据
jQuery.prototype实例方法: $("x").data("p1");
jQuery静态方法:jQuery.data(elem, "p1");
(2) 读全部数据
jQuery.prototype实例方法: $("x").data();
jQuery静态方法:jQuery.data(elem);
3 删数据
jQuery.prototype实例方法: $("x").removeData("p1");
jQuery静态方法:jQuery.removeData(elem, "p1");
二 设计方案
jQuery Data模块设计思路相对其他模块而言是比较简单的,这里概略总结一下。
1 对于原生dom对象
首先排除那些不能接受data缓存的dom元素类型,这些类型被标记于noData对象中,只要dom对象节点名称和对应属性符合noData集合规则的就是不能接受data缓存的dom类型:'var match = jQuery.noData[ elem.nodeName.toLowerCase() ];',对应acceptData()方法将返回false。
对于那些能接受data缓存的dom对象,每个对象的数据缓存在全局缓存对象jQuery.cache中,而不是dom对象本身中!这一点是关键点,在全局缓存对象jQuery.cache中为每一个dom对象开辟一个缓存对象缓存该dom的data数据,由此引出一个问题是:如何关联上不同的dom对象到全局缓存对象jQuery.cache的不同缓存对象上?jQuery的方案是给每一个dom对象添加一个属性,该属性名称为'jQuery.expando'变量的值,这个值由三个部分组成:'jQuery'、'version'、'random数字值',其规则是'expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ),',这个变量的值这样设计是为了最大化保证不与用户自定义变量值雷同而引起冲突,所有能缓存data的dom对象的该属性名称都是一致的;但是每个dom对象该属性的值却是唯一的,类似于关系型数据库表的主键规则,由一个全局uuid唯一生成'elem[ jQuery.expando ] = id = ++jQuery.uuid;',这个值是唯一的且递增的整数值。
dom对象属性'jQuery.expando'的值'jQuery.uuid'将作为全局缓存对象的key,全局缓存对象的value就是独立的一个缓存对象thisCache,存储对应dom对象需要缓存的所有data。这样根据每个不同的dom对象的属性值就能在全局缓存对象jQuery.cache中唯一对应上一个独立缓存对象,对dom对象缓存data的CRUD操作说白了就是对这个独立缓存对象thisCache的CRUD操作。
2 对于js内核对象
js内核对象的缓存就没那么绕了,因为js内核对象本身就可以开辟一个属性作为独立缓存对象thisCache,这个属性的名称也简单,就是'jQuery.expando'的值,所有js内核对象的缓存属性的名称都是一致的。至于dom对象为什么不能向js内核对象一样直接存储缓存数据,源码中给出的解释原因是涉及到浏览器的内存回收机制--如果直接在dom对象上添加独立缓存对象,那么这个dom对象由于引用了独立缓存对象,垃圾回收机制根据'引用计数法'将无法判定该dom对象是可以被回收的,这样长时间运行积累下来可能导致OOM(这个理解可能不准确)。
三 主要方法的源码分析
1 jQuery.data( elem, name, data, pvt /* Internal Use Only */ )方法源码
由于data一个方法就承担数据操作的C/R/U三种类型,所以其参数调用和内部实现逻辑比较多样化。
data: function( elem, name, data, pvt /* Internal Use Only */ ) {
/** 对于不能读写的dom类型对象直接返回。*/
if ( !jQuery.acceptData( elem ) ) {
return;
}
/**
确定该方法是读还是写--name参数和data参数都是undefined的话是读全部,name参数为string类型且data为undefined的话是读单条,其他调用是写。
确定该方法是对dom对象操作还是js对象操作--直接看elem参数有没有nodeType属性。
确定当前elem对象的id--是dom对象的话就是elem[ jQuery.expando ],否则直接为jQuery.expando。
确定父级缓存对象cache--是dom对象的话就是全局缓存对象jQuery.cache,否则就是elem本身。*/
var internalKey = jQuery.expando, getByName = typeof name === "string", thisCache,
// 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,
// 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,
// 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[ jQuery.expando ] : elem[ jQuery.expando ] && jQuery.expando;
/** 如果是读方法但是id为空的话(说明还未存储过data)直接返回。 */
// 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 || (pvt && id && !cache[ id ][ internalKey ])) && getByName && data === undefined ) {
return;
}
/** 如果id不存在为其构造一个--是dom对象的话由全局uuid生成唯一值,否则直接为jQuery.expando。 */
if ( !id ) {
// Only DOM nodes need a new unique ID for each element since their data
// ends up in the global cache
if ( isNode ) {
elem[ jQuery.expando ] = id = ++jQuery.uuid;
} else {
id = jQuery.expando;
}
}
/** 如果子级缓存对象cache[ id ]不存在为其构造一个空的对象--非dom的js内核对象需要为其子级缓存对象引入一个toJSON属性。 */
if ( !cache[ id ] ) {
cache[ id ] = {};
// TODO: This is a hack for 1.5 ONLY. Avoids exposing jQuery
// metadata on plain JS objects when the object is serialized using
// JSON.stringify
if ( !isNode ) {
cache[ id ].toJSON = jQuery.noop;
}
}
/** 如果是写多条数据的调用,则把需要写入的数据扩展到子级缓存对象thisCache中去。 */
// 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" ) {
if ( pvt ) {
cache[ id ][ internalKey ] = jQuery.extend(cache[ id ][ internalKey ], name);
} else {
cache[ id ] = jQuery.extend(cache[ id ], name);
}
}
/** 判定子级缓存对象thisCache--是dom对象的话为jQuery.cache[ id ],否则就是elem[ id ]。 */
thisCache = cache[ id ];
// Internal jQuery data is stored in a separate object inside the object's data
// cache in order to avoid key collisions between internal data and user-defined
// data
if ( pvt ) {
if ( !thisCache[ internalKey ] ) {
thisCache[ internalKey ] = {};
}
thisCache = thisCache[ internalKey ];
}
/** 如果是写单条数据的调用,则写入一条数据到子级缓存对象thisCache上。 */
if ( data !== undefined ) {
thisCache[ jQuery.camelCase( name ) ] = data;
}
// TODO: This is a hack for 1.5 ONLY. It will be removed in 1.6. Users should
// not attempt to inspect the internal events object using jQuery.data, as this
// internal data object is undocumented and subject to change.
if ( name === "events" && !thisCache[name] ) {
return thisCache[ internalKey ] && thisCache[ internalKey ].events;
}
/** 如果是读单条/多条数据的调用,则返回一条数据/全部数据。 */
return getByName ? thisCache[ jQuery.camelCase( name ) ] : thisCache;
},
2 jQuery.removeData( elem, name, pvt /* Internal Use Only */ )方法源码
removeData方法只承担数据操作的D类型,相对功能单一,方法内部流程类似,不同的是当子级独立缓存对象内部已经没有缓存的数据了时需要delete掉该独立缓存对象本身以及dom对象的对应属性。
removeData: function( elem, name, pvt /* Internal Use Only */ ) {
if ( !jQuery.acceptData( elem ) ) {
return;
}
var internalKey = jQuery.expando, isNode = elem.nodeType,
// See jQuery.data for more information
cache = isNode ? jQuery.cache : elem,
// See jQuery.data for more information
id = isNode ? elem[ jQuery.expando ] : jQuery.expando;
// If there is already no cache entry for this object, there is no
// purpose in continuing
if ( !cache[ id ] ) {
return;
}
if ( name ) {
var thisCache = pvt ? cache[ id ][ internalKey ] : cache[ id ];
if ( thisCache ) {
delete thisCache[ name ];
// If there is no data left in the cache, we want to continue
// and let the cache object itself get destroyed
if ( !isEmptyDataObject(thisCache) ) {
return;
}
}
}
// See jQuery.data for more information
if ( pvt ) {
delete cache[ id ][ internalKey ];
// Don't destroy the parent cache unless the internal data object
// had been the only thing left in it
if ( !isEmptyDataObject(cache[ id ]) ) {
return;
}
}
var internalCache = cache[ id ][ internalKey ];
// Browsers that fail expando deletion also refuse to delete expandos on
// the window, but it will allow it on all other JS objects; other browsers
// don't care
if ( jQuery.support.deleteExpando || cache != window ) {
delete cache[ id ];
} else {
cache[ id ] = null;
}
// We destroyed the entire user cache at once because it's faster than
// iterating through each key, but we need to continue to persist internal
// data if it existed
if ( internalCache ) {
cache[ id ] = {};
// TODO: This is a hack for 1.5 ONLY. Avoids exposing jQuery
// metadata on plain JS objects when the object is serialized using
// JSON.stringify
if ( !isNode ) {
cache[ id ].toJSON = jQuery.noop;
}
cache[ id ][ internalKey ] = internalCache;
// Otherwise, we need to eliminate the expando on the node to avoid
// false lookups in the cache for entries that no longer exist
} else if ( isNode ) {
// IE does not allow us to delete expando properties from nodes,
// nor does it have a removeAttribute function on Document nodes;
// we must handle all of these cases
if ( jQuery.support.deleteExpando ) {
delete elem[ jQuery.expando ];
} else if ( elem.removeAttribute ) {
elem.removeAttribute( jQuery.expando );
} else {
elem[ jQuery.expando ] = null;
}
}
},
四 内部私有数据/二级缓存方案
如果调用方希望缓存的data不是外部可访问的,jQuery Data对于这些数据的存储地点是单独维护的,位于子级独立缓存对象的内部名称同样为'jQuery.expando'的属性值对象中,相当于是一个二级缓存方案,jQuery Data模块的调用者知道怎么访问和存入数据,其他用户则不关心这块数据,这是_data()方法通过调用'jQuery.data( elem, name, data, true );'方法并传入第四个参数pvt为true来实现的,jQuery.data()方法内部有独立的逻辑判断并处理私有数据。
jQuery Data模块的实现是比较通用的,即它支持在dom对象/内核对象上存储"任意类型"的缓存数据,这些缓存数据将用于上层模块,按照上层模块的意图和需求,这些缓存数据可以是任意类型的。比如对jQuery Event模块的需求而言,在选择集dom元素上存入的缓存数据是"可执行函数集合",这个时候jQuery Data模块中缓存的数据对象的作用就非常类似jQuery Deffered模块中的回调函数存储对象了,也即它扮演的是"弹夹"的角色,按照每个dom元素事件类型分别存储一个事件回调函数队列,就像不同口径不同规格的子弹被各自的弹夹存放一样。对于其他模块而言,不一定只存储函数数据,完全可以根据模块设计者的需求存储其他类型数据。后续分析jQuery Event模块时将深入该主题。