create: 2013-08-24 22:09
jQuery 中的 data 部分用于将数据与 DOM 节点进行关联。
举个简单的例子,假如我们的 HTML 代码中有如下内容:
<button id="btn">Test</button>
接下来,在 JavaScript 代码中:
var btn = $('#btn');
btn.data('type', 'button');
btn.on('click', function() {
console.log(btn.data('type'));
})
这样,当你点击按钮时,控制台便会输出 button 字符串。
除了用于增加和获取数据的 data() 方法,此外还有用于判断数据是否存在的 jQuery.hasData() 方法,清除数据的 removeData() 方法,整个 data 部分的 API 就这么简单。(data() 与 removeData() 同时为 jQuery 的静态和实例方法)
原理
jQuery 是如何将数据和 DOM 元素关联起来的?说起来可简单了,就是使用了最常见的 JavaScript 对象。
关于 Object 我们太熟悉了,可以说 JavaScript 中一切皆为对象,我们只需
var o = {};
就创建了一个对象 o,而对象可以拥有属性,属性名为字符串,属性值则是任意类型:
o.button = {};
o.button.type = 'button';
我们假设 o.button 就是上面的 button 元素,那么我们是不是就已经把 DOM 元素和数据 type 关联在一起了?实际上 jQuery 内部的处理就是如此,jQuery 有一个 cache 对象就相当于我们定义的对象 o,它的作用就是保存 DOM 元素和数据之间的关联。
区分 DOM 元素
如果我们有两个 DOM 元素 btn1 和 btn2,二者都是 button 类型,我们进行如下处理:
btn1.data('content', 'Hello');
btn2.data('content', 'World');
那么 jQuery 内部是如何区分这两个 button 的,如果是你该如何来做?
哈,你肯定会想到,既然是两个不同的元素,那么我给它们分别分配一个独一无二的标识符来区分不就可以了吗?而在 core.js 中,恰好就定义了这么个标识符:jQuery.guid。
jQuery.guid 的初始值为 1,而且不断递增。
我们来写点代码:
btn1.dataId = jQuery.guid++;
btn2.dataId = jQuery.guid++;
jQuery.cache[btn1.dataId] = { content: 'Hello' };
jQuery.cache[btn2.dataId] = { content: 'World' };
瞧,btn1 和 btn2 的 dataId 不同,通过它就可以找到 cache 上对应的对象。只不过 jQuery 不会用 dataId 这么挫的名字来作为元素的标识符啦~至于是啥,看源码就清楚了!
acceptData()
在知道了原理,了解了如何区分 DOM 对象后,我们便可以阅读源码了。
但是我先提个问题,是不是所有的 DOM 对象都可以关联数据呢?
答案是否定的!
我们先看源码中执行 jQuery.extend 的部分:
cache: {},
expando: "jQuery" + (core_version + Math.random()).replace(/\D/g, ""),
noData: {
"embed": true,
"object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",
"applet": true
},
cache 我们已经知道了,是真正存储数据的对象。
expando,看名字就明白啦,它便是上面提到的 dataId 的替代者,至于为什么不用固定的名字,那是因为 jQuery 是可以多版本共存的,如果所有版本都用相同的 expando,那么使用某版本 jQuery 设置的数据就有可能被另一个版本 jQuery 的操作给覆盖掉,实在是够乱。
而 noData 对象则标明了哪些元素是不能关联数据的,包括了:
- embed 元素
- object 元素
- applet 元素
object 后面的内容是 Flash 的 classid,即除了 Flash 外,所有通过 embed、object、applet 标签引入的元素都无法关联数据,原因在 jQuery 的注释中解释了:为这些元素设置 expando 属性会引发无法捕获的异常。
知道了这些内容,acceptData() 方法中的逻辑便清晰了:
// 不要为任何非元素节点设置数据,因为数据将无法被清除 (#8335)。
if (elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9) {
return false;
}
var noData = elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()];
return !noData || noData !== true && elem.getAttribute("classid") === noData;
先排除所有非元素的节点,nodeType 为 1 表示 Element,9 表示 Document。
再排除 noData 中列出的元素,当然对 object 要特殊处理。
internalData()
查看 jQuery.extend 中的其他内容你会发现,这些静态方法基本上都依赖于另外两个内部方法:internalData() 和 internalRemoveData(),仅仅是传入的参数不同。
internalData() 方法用于存储和读取数据。
function internalData(elem, name, data, pvt /* 仅限内部使用 */ )
看参数列表就能明白,如果传入 data,表示设置数据,如果不传,则是读取数据,但是最后那个仅限内部使用的 pvt 参数是干什么的?
要知道 jQuery 内部同样会使用 data() 这样的方法来对元素进行数据操作,例如后面会讲到的关于事件处理部分。那这样就引发了一个问题,jQuery 如何来区分当前操作的数据是 jQuery 自己设置的,还是用户设置的?
解决办法就是将 jQuery 自己设置的数据和用户设置的数据分开。
jQuery.cache[btn1.dataId] = {};
这是前面用过的示例代码,实际上这个代码并不完整,最接近真实情况的代码是:
jQuery.cache[btn1.dataId] = { data: {} };
看,最外层的对象是给 jQuery 自己用的,这个对象的 data 属性则是留给用户的,就是说,当 pvt 为 true,jQuery 就去操作外层对象,如果为 false,就去操作里面的 data 对象,如此一来,就不怕操作数据的时候冲突了。
除了这个内部使用的参数外,jQuery 还定义了直供内部使用的 data() 和 removeData() 两个方法,只有在这两个方法内才会去传入 pvt 参数。
来看 internalData() 的源码:
if (!jQuery.acceptData(elem)) {
return;
}
这句话过滤掉了无法设置数据的元素。
var thisCache, ret,
internalKey = jQuery.expando,
getByName = typeof name === "string",
// 我们需要对 DOM 节点和 JS 对象分别处理,因为 IE6-7 无法正确回收 DOM 和 JS 互相引用的对象
isNode = elem.nodeType,
// 只有 DOM 节点需要全局 jQuery 缓存;
// JS 对象的数据直接绑定在对象本身,这样垃圾回收能够自动处理
cache = isNode ? jQuery.cache : elem,
id = isNode ? elem[internalKey] : elem[internalKey] && internalKey;
上面这段内容也比较简单,但是揭露了一个我没有提到的问题,那就是 data() 方法并非只能操作 DOM 元素,你可以传入 JavaScript 对象:
var o = {};
jQuery.data(o, 'type', 'object');
由于 IE 6-7 的问题,如果你在 DOM 节点上直接绑定 JavaScript 对象的话,垃圾回收器很可能会因为两者的循环引用而无法回收对象,因此造成内存泄露,而 JavaScript 对象则没有这方面的担忧,所以 jQuery 就直接将数据绑定到对象本身,而并非 jQuery.cache 中。
if ((!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined) {
return;
}
如果元素或对象没有关联过数据,那么就不做操作(这里指读取数据操作)。
if (!id) {
if (isNode) {
elem[internalKey] = id = core_deletedIds.pop() || jQuery.guid++;
} else {
id = internalKey;
}
}
如果 id 不存在,表明元素或对象没有关联过数据,而只有元素是需要在全局缓存的,因此要使用 guid,而 core_deletedIds 则是在 core.js 中定义的一个数组,用于放置废弃的 guid,这样就可以很有效率的利用 guid。
if (!cache[id]) {
cache[id] = {};
if (!isNode) {
cache[id].toJSON = jQuery.noop;
}
}
这里要提到的是 toJSON,这是保证对 JavaScript 对象进行序列化操作时不会将关联的数据也序列化出来。
if (typeof name === "object" || typeof name === "function") {
if (pvt) {
cache[id] = jQuery.extend(cache[id], name);
} else {
cache[id].data = jQuery.extend(cache[id].data, name);
}
}
有时候要关联的数据可能是个对象而并非键值对,这时候需要执行浅拷贝。
if (!pvt) {
if (!thisCache.data) {
thisCache.data = {};
}
thisCache = thisCache.data;
}
理解 pvt 的含义。
if (data !== undefined) {
thisCache[jQuery.camelCase(name)] = data;
}
设置数据。你可能注意到了 jQuery.camelCase() 方法,它是用于将类似 "a-b" 转换为 "aB" 形式,为什么需要这么做?后面讲到 data 属性时你就明白了。
if (getByName) {
ret = thisCache[name];
if (ret == null) {
ret = thisCache[jQuery.camelCase(name)];
}
} else {
ret = thisCache;
}
return ret;
这是在获取数据,如果直接通过 name 获取不到的话,会尝试使用驼峰形式的名字再次获取一遍,最终返回结果。
internalData() 函数讲解完毕。
internalRemoveData()
其实这两个方法都不复杂,只要你了解 API 的用法,那么对源码中的逻辑也就会很清楚。
对于 jQuery.removeData(),你需要传入 element,如果传入 name,表示将与该 name 关联的数据删除,如果不传,那么就清除 element 上的所有数据。
来看下 internalRemoveData() 方法。
代码的前部分和 internalData() 类似,不赘述。
// 如果该对象没有缓存条目,那么就没有必要继续了
if ( !cache[ id ] ) {
return;
}
if ( name ) {
//获取数据缓存
thisCache = pvt ? cache[ id ] : cache[ id ].data;
if ( thisCache ) {
// 支持数据键名为数组或空格分隔的字符串
if ( !jQuery.isArray( name ) ) {
// 在操作之前尝试以字符串作为键名
if ( name in thisCache ) {
name = [ name ];
} else {
// 如果驼峰格式的键名不存在,那么尝试使用空格来拆分字符串、
name = jQuery.camelCase( name );
if ( name in thisCache ) {
name = [ name ];
} else {
name = name.split(" ");
}
}
} else {
// #12786 这里提到了之前的一个 bug,那就是使用 'a-a', 'b-b' 作为键名存储数据
// 再用 removeData('a-a b-b') 删除,是没有问题的
// 但使用 removeData([ 'a-a', 'b-b' ]) 却无法删除
// 于是对于数组参数,将同时删除普通格式和驼峰格式的键名。
name = name.concat( jQuery.map( name, jQuery.camelCase ) );
}
// 删除键名对应的数据
for ( i = 0, l = name.length; i < l; i++ ) {
delete thisCache[ name[i] ];
}
// 如果缓存中没有数据存在了,那么继续运行,并将缓存对象销毁
if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) {
return;
}
}
}
在最后一段内容中,判断了缓存对象是否为空,这里用到了两个方法,当然先说说它们的调用形式。
比如说有两个函数:
function a(c) { alert(c); }
function b(c) { alert(c); }
如果我们还有一个变量 d,如果 d 为 true,就调用 a 方法,如果为 false 就调用 b 方法,那么就可以使用三目运算符 ? : 来替代 if-else 操作:
// if-else
if(d) {
a(c);
} else {
b(c);
}
// 三目运算符
(d ? a : b)(c);
好,回到源码,在判断对象是否为空处使用了两个函数,其中一个在之前介绍 core.js 的续篇中有讲到,而另一个 isEmptyDataObject 则为内部方法,用于判断内部数据缓存对象是否为空。
function isEmptyDataObject( obj ) {
var name;
for ( name in obj ) {
// 如果公共数据对象为空,那么私有数据对象亦为空
if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) {
continue;
}
if ( name !== "toJSON" ) {
return false;
}
}
return true;
}
这个函数很简单,不说了。继续看 internalRemoveData() 的源码。
// 前面已经提到了,如果缓存对象为空,那么函数继续运行
if ( !pvt ) {
delete cache[ id ].data;
// 如果父级缓存对象中只包含内部数据对象,那么将父级缓存对象销毁,否则退出函数
if ( !isEmptyDataObject( cache[ id ] ) ) {
return;
}
}
// 销毁缓存,这个 cleanData() 方法不在当前讨论范围内
if ( isNode ) {
jQuery.cleanData( [ elem ], true );
// 如果支持 expandos 或 `cache` 不为 window 对象,使用 delete (#10080)
} else if ( jQuery.support.deleteExpando || cache != cache.window ) {
delete cache[ id ];
} else {
cache[ id ] = null;
}
这里面提到了 deleteExpando 和 delete 的问题,这属于 IE 的 bug,详细信息参考这篇精彩博客 http://hax.iteye.com/blog/349569。
以上我们基本将与 data 有关的静态方法源码看了一遍,接下来开始看和实例方法相关的内容。
data() 和 removeData()
removeData: function( key ) {
return this.each(function() {
jQuery.removeData( this, key );
});
}
removeData() 内部就是循环调用的 jQuery.removeData(),因此略过。
data: function( key, value ) {
var attrs, name,
//因为是读取数据,所以只操作元素集合中的第一个
elem = this[0],
i = 0,
data = null;
// 获取所有数据
if ( key === undefined ) {
if ( this.length ) {
data = jQuery.data( elem );
// 对于元素对象特殊处理
if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) {
attrs = elem.attributes;
for ( ; i < attrs.length; i++ ) {
name = attrs[i].name;
if ( !name.indexOf( "data-" ) ) {
name = jQuery.camelCase( name.slice(5) );
dataAttr( elem, name, data[ name ] );
}
}
jQuery._data( elem, "parsedAttrs", true );
}
}
return data;
}
// 设置多个值
if ( typeof key === "object" ) {
return this.each(function() {
jQuery.data( this, key );
});
}
return arguments.length > 1 ?
// 设置一个值
this.each(function() {
jQuery.data( this, key, value );
}) :
// 获取一个值
elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null;
}
你可能会发现代码中出现的 "parsedAttrs","data-",dataAttr() 这样的内容,这里面涉及到了 HTML5 中的新内容:自定义数据特性(data-*)。
所谓自定义数据特性,就是允许你在 HTML 标签上使用 data- 作为前缀的特性,来存储数据。可能你之前也在 HTML 标签上使用过自定义的特性,但 HTML5 为以 data- 为前缀的特性增加了更便捷的存取方法。
http://html5doctor.com/html5-custom-data-attributes/ HTML5 Doctor 对该特性做了讲解,其中提到了获取自定义数据的方法,除了使用通用的 getAttribute() 外,你还可以访问元素对象的 dataset 属性,例如:
<div id='sunflower' data-leaves='47' data-plant-height='2.4m'></div>
对于上面的 HTML 标签,若想获取数据,你可以这么做:
var plant = document.getElementById('sunflower');
var leaves = plant.dataset.leaves; // leaves = 47;
var tallness = plant.dataset.plantHeight;
这比通过特性名来获取值更为方便,不过注意的是,data-plant-height 被转换成了驼峰形式:plantHeight,所以你可能也明白了,为什么上面代码中经常会出现将属性名转为驼峰形式的代码。
在 data() 方法中:
if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) {
这段代码开始,就是为了处理元素节点上以 data- 开头的特性,需要将这些数据存储到 jQuery 的缓存对象中。
而 dataAttr() 函数所做的内容就是将元素以 data- 开头的特性存储到缓存对象中,并返回数据。
因为 HTML 特性只能为字符串,所以 dataAttr() 中所做的大部分内容就是将字符串转为原始格式,包括:boolean、null、number、object。
其中判断 number 是先将字符串转为数字,再转为字符串和原始字符串比较,如果相同,则认为是数字。
如果字符串中包含花括号,可以认为它是 JSON 格式,调用 jQuery.parseJSON() 解析。
最后要注意的是,dataAttr() 不会覆盖已经设置过的数据,就是说如果缓存对象中存在了某个 key,而 HTML 特性上也拥有同样的 key,那么 HTML 特性值不会覆盖缓存对象中的对应值。
啰嗦完了(撒花╮(╯_╰)╭)