jQuery在1.2后引入jQuery.data(数据缓存系统),主要的作用是让一组自定义的数据可以DOM元素相关联——浅显的说:就是让一个对象和一组数据一对一的关联。
一组和Element相关的数据如何关联着这个Element一直是web前端的大姨妈,而最初的jQuery事件系统照搬Dean Edwards的addEvent.js:将回调挂载在EventTarget上,这样下来,循环引用是不可忽视的问题。而在web前端中,数据和DOM的关系太过基情和紧张,于是jQuery在1.2中,正式缔造了jQuery.data,就是为了解决这段孽缘:自定义数据和DOM进行关联。
文中所说的Element主要是指数据挂载所关联的target(目标),并不局限于Element对象。
这篇文章主要分为以下知识
jQuery.data模型
模型
凡存在,皆真理——任何一样事物的存在必然有其存在的理由,于我们的角度来说,这叫需求。
一组数据,如何与DOM相关联一直是web前端的痛处,因为浏览器的兼容性等因素。最初的jQuery事件系统照搬Dean Edwards的addEvent.js:将回调挂载在EventTarget上,这样下来,循环引用是不可忽视的问题,它把事件的回调都放在相应的EventTarget上,当回调中再引用EventTarget的时候,会造成循环引用。于是缔造了jQuery.data,在jQuery.event中通过jQuery.data挂载回调函数,这样解决了回调函数的循环引用,随时时间的推移,jQuery.data应用越来越广,例如后来的jQuery.queue。
首先我们要搞清楚jQuery.data解决的需求,有一组和DOM相关/描述Element的数据,如何存放和挂载呢?可能有人是这样的:
使用attributes
HTML:
<div id="demo" userData="linkFly"></div>
javascript:
(function () {
var demo = document.getElementById('demo');
console.log(demo.getAttribute('userData'));
})();
使用HTML5的dataset
HTML:
<div id="demo2" data-user="linkFly"></div>
javascript:
(function () {
var demo = document.getElementById('demo2');
console.log(demo.dataset.user);
})();
为DOM实例进行扩展
HTML:
<div id="demo3"></div>
javascript:
(function () {
var demo = document.getElementById('demo3');
demo.userData = 'demo';
console.log(demo.userData);
})();
- 1、只能保存字符串(或转化为字符串类型)的数据,同时曝露了数据,并且在HTML上挂载了无谓的属性,浏览器仍然会尝试解析这些属性。
- 2、同上。
- 3、典型的污染,虽然可以保存更强大的数据(Object/Function),但是患有代码洁癖的骚年肯定是不能忍的,更主要,如果挂载的数据中引用着这个Element,则会循环引用。
模型
一窥模型吧,jQuery.data在早期,为了兼容性做了很多的事情。同时,或许是因为jQuery.data最初的需求作者也觉得太过简单,所以实现的代码上让人觉得略显仓促,早期的数据仓库很是繁琐,在jQuery.2.x后,jQuery.data重写,同时终于把jQuery.data抽离出对象。
jQuery.data模型上,就是建立一个数据仓库,而每一个挂载该数据的对象,都有自己的钥匙,他和上面的代码理念并不同:
-
上面的方案是:
在需要挂载数据的对象上挂载数据,就好像你身上一直带着1000块钱,要用的时候直接从口袋里掏就可以了。
-
jQuery.data则是:
建立一个仓库,所有的数据都放在这个仓库里,然后给每个需要挂载数据的对象一把钥匙,读取数据的时候拿这个钥匙到仓库里去拿,就好像所有人都把钱存在银行里,你需要的时候则拿着银行卡通过密码去取钱。
图一张:
我们暂时先不讨论数据仓库的样子,首先我们要关注数据和Element关联的关键点——钥匙,这个钥匙颇具争议,后续的几种数据缓存方式都是在对这个钥匙进行大的变动,因为这个钥匙,不得不放在Element上——即使你把所有的钱都存在银行里了,但是你身上还是要有相应的钥匙,这不得不让那些代码洁癖的童鞋面对这个问题:Element注定要被污染——jQuery.data只是尝试了最小的污染。
jQuery在创建的时候,会生成一个属性——jQuery.expando:
expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" );
jQuery.expando是当前页面中引用的jQuery对象的身份标志(id),每个页面引用的jQuery.expando都是不重复且唯一的,所以这就是钥匙的关键:jQuery.expando生成的值作为钥匙,挂载在Element上,也就是为Element创建一个属性:这个属性的名称,就是jQuery.expando的值,这就是钥匙的关键。 虽然仍然要在Element上挂载自己的数据,但是jQuery尽可能做到了最小化影响用户的东西。
当然这里需要注意:通过为Element添加钥匙的时候,使用的是jQuery.expando的值作为添加的属性名,页面每个使用过jQuery.data的Element上都有jQuery.expando的值扩展的属性名,也就是说,每个使用过jQuery.data的Element都有这个扩展的属性,通过检索这个属性值来找到仓库里的数据——钥匙是这个属性值,而不是这个jQuery.expando扩展的属性名。
木图木真相:
jQuery.1.x中jQuery.data实现
这里的jQuery.1.x主要是指jQuery.1.11。
jQuery.acceptData() - 目标过滤
因为jQuery.1.x是兼容低版本浏览器的,所以需要处理大量的浏览器兼容性,在jQuery.1.x中设计的jQuery.data是基于给目标添加属性来实现的,所以这其中找属性找钥匙找仓库很是繁琐,再加上IE低版本各种雷区,简直丧心病狂了已经。找钥匙找仓库还好说,低版本IE的雷区一踩一个爆:所以jQuery单独写了一个jQuery.acceptData用于屏蔽雷区,特别针对下面的情况:
- applet和embed:这两个标签都是加载外部资源的,这两个标签在js里可以操作的权限简直就是缩卵了——根本行不通,所以jQuery直接给干掉了,直接让他们不能放标签。
- flash:早期的jQuery将所有的Object标签纳入雷区,后来发现IE下的flash还是可以自定义属性的,于是针对IE的flash还是网开一面,放过了IE的flash,IE下加载flash的时候,需要对object指定一个叫做classId的属性,它的值为:clsid:D27CDB6E-AE6D-11cf-96B8-444553540000。
jQuery.acceptData配合jQuery.noData做的过滤:
jQuery.extend({
//jQuery.cache对象,仓库
cache: {},
noData: {
//有可能权限不够,所以过滤
"applet ": true,
"embed ": true,
//ie的flash可以通过
"object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
}
//其余API代码略
}); jQuery.acceptData = function (elem) {
//确定一个对象是否允许设置Data
var noData = jQuery.noData[(elem.nodeName + " ").toLowerCase()],
nodeType = +elem.nodeType || 1;
//进行过滤
return nodeType !== 1 && nodeType !== 9 ?
false :
//如果是jQuery.noData里面限定的节点的话,则返回false
//如果节点是object,则判定是否是IE flash的classid
!noData || noData !== true && elem.getAttribute("classid") === noData;
};
internalData() - 挂载/读取数据
挂载和读取数据方法是同一个(下面有分步分析):首先拿到钥匙,也就是jQuery.expando扩展的属性,然后根据钥匙获取仓库,因为内部数据和用户数据都是挂载在jQuery.cache下的,所以在jQuery.cache下开辟了jQuery.cache[钥匙].data作为用户数据存放的空间,而jQuery.cache[钥匙]则存放jQuery的内部数据,将数据挂上之后,返回的结果是这个数据挂载的空间/位置,通过这个返回值可以访问到这个Element所有挂载的数据。
function internalData(elem, name, data, pvt /* Internal Use Only */) {
//pvt:标识是否是内部数据 //判定对象是否可以存数据
if (!jQuery.acceptData(elem)) {
return;
}
var ret, thisCache,
//来自jQuery随机数(每一个页面上唯一且不变的)
internalKey = jQuery.expando,
/*
如果是DOM元素,
为了避免javascript和DOM元素之间循环引用导致的浏览器(IE6/7)垃圾回收机制不起作用,
要把数据存储在全局缓存对象jQuery.cache中
*/
isNode = elem.nodeType,
//只有DOM节点才需要全局缓存,js对象是直接连接到对象的
//如果是DOM,则cache连接到jQuery.cache
cache = isNode ? jQuery.cache : elem,
//如果是DOM,则获取钥匙,如果是第一次读取,则读取不到钥匙
id = isNode ? elem[internalKey] : elem[internalKey] && internalKey;
//检测合法性,避免做更多的工作,pvt标识是否是内部数据
if ((!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string") {
return;
}
//没有拿到钥匙,则赋上ID
if (!id) {
if (isNode) {
/*
DOM需要有一个全新的全局id,
为DOM建立一个jQuery的全局id
低版本代码:elem[ internalKey ] = id = ++jQuery.uuid;
这个deletedIds暂时忽略,当初jQuery准备重用uuid,后来被guid取代了
*/
id = elem[internalKey] = deletedIds.pop() || jQuery.guid++;
} else {
//对象就不用这么麻烦了,直接挂钥匙就可以了
id = internalKey;
}
}
//从jQuery.cache中没有读取到,则开辟一个新的
if (!cache[id]) {
//创建一个新的cache对象,这个toJson是个空方法
cache[id] = isNode ? {} : { toJSON: jQuery.noop };
/*
对于javascript对象,设置方法toJSON为空函数,
以避免在执行JSON.stringify()时暴露缓存数据。
如果一个对象定义了方法toJSON()
JSON.stringify()在序列化该对象时会调用这个方法来生成该对象的JSON元素
*/
} /*
先把Object/Function的类型的数据挂上。调用方式 :
$(Element).data({'name':'linkFly'});
这里的判定没有调用jQuery.type()...当然在于作者的心态了...
*/
if (typeof name === "object" || typeof name === "function") {
if (pvt) {//如果是内部数据
//挂到cache上
cache[id] = jQuery.extend(cache[id], name);
} else {
//如果是自定义数据,则挂到data上
cache[id].data = jQuery.extend(cache[id].data, name);
}
}
//调整位置,因为有可能是取数据
thisCache = cache[id]; if (!pvt) {
//如果不是内部数据(即用户自定义数据),则调整到jQuery.chche.data上
if (!thisCache.data) {
thisCache.data = {};
}
//继续调整位置
thisCache = thisCache.data;
}
/*
到了这里外面的调用方式是
$(Element).data('name','value');
*/
if (data !== undefined) {
//挂上数据
thisCache[jQuery.camelCase(name)] = data;
}
if (typeof name === "string") {
ret = thisCache[name];
if (ret == null) {
ret = thisCache[jQuery.camelCase(name)];
}
} else {
ret = thisCache;
} //同时处理获取数据的情况
return ret;
}
太长看起来恶心?来,我们一点点分析:
1、首先,检测是否可以存放数据,可以的话初始化操作,针对变量id要注意,这里的一直在找上面我们所说的挂载在Element上那个存放钥匙的属性,也就是jQuery.expando的值
if (!jQuery.acceptData(elem)) {
return;
}
var ret, thisCache,
//来自就jQuery随机数(每一个页面上唯一且不变的)
internalKey = jQuery.expando,
/*
如果是DOM元素,
为了避免javascript和DOM元素之间循环引用导致的浏览器(IE6/7)垃圾回收机制不起作用,
要把数据存储在全局缓存对象jQuery.cache中;
对于javascript对象有垃圾回收机制
所以不会有内存泄露的问题
因此数据可以直接存储在javascript对象上
*/
isNode = elem.nodeType,
//如果是Element,则cache连接到jQuery.cache
cache = isNode ? jQuery.cache : elem,
//如果是Element,则获取钥匙
id = isNode ? elem[internalKey] : elem[internalKey] && internalKey;
//检测合法性,第一次调用$(Element).data('name')会被拦截
if ((!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string") {
return;
}
2、如果没有钥匙(id),则为目标添加上钥匙,代码如下:
//没有ID,则赋上ID
if (!id) {
if (isNode) {
/*
DOM需要有一个全新的全局id
为DOM建立一个jQuery的全局id
低版本代码:elem[ internalKey ] = id = ++jQuery.uuid;
这个deletedIds暂时忽略
id = elem[internalKey] = deletedIds.pop() || jQuery.guid++;
*/
} else {
//而对象不需要
id = internalKey;
}
}
2、根据钥匙,尝试从cache中读仓库的位置,如果仓库中还没有这个目标的存放空间,则开辟一个,这里特别针对了JSON做了处理:当调用JSON.stringify序列化对象的时候,会调用这个对象的toJSON方法,为了保证jQuery.data里面数据的安全,所以直接重写toJSON为一个空方法(jQuery.noop),这样就不会曝露jQuery.data里面的数据。另外一种说法是针对HTML5处理的dataAttr()(下面有讲)使用JSON.parse转换Object对象,而这个JSON可能是JSON2.js引入的:JSON2.js会为一系列原生类型添加toJSON方法,导致for in循环判定是否为空对象的时候无法正确判定——所以jQuery搞了个jQuery.noop来处理这里。
//从cache中没有读取到
if (!cache[id]) {
//创建一个新的cache对象,这个toJson是个空方法
cache[id] = isNode ? {} : { toJSON: jQuery.noop };
/*
对于javascript对象,设置方法toJSON为空函数,
以避免在执行JSON.stringify()时暴露缓存数据。
如果一个对象定义了方法toJSON()
JSON.stringify()在序列化该对象时会调用这个方法来生成该对象的JSON元素
*/
}
3、如果是Function/Object,则直接调用jQuery.extend挂数据,把$(Element).data({'name':'linkFly'})这种调用方式的数据挂到jQuery.cache
/*
先把Object/Function的类型的数据挂上。调用方式 :
$(Element).data({'name':'linkFly'});
这里的判定没有调用jQuery.type()...当然在于作者的心态了...
*/ if (typeof name === "object" || typeof name === "function") {
if (pvt) {//如果是内部数据
//挂到cache上
cache[id] = jQuery.extend(cache[id], name);
} else {
//如果是自定义数据,则挂到data上
cache[id].data = jQuery.extend(cache[id].data, name);
}
}
4、还有其他数据类型(String之类的)没有挂载上,这里把$(Element).data('name','value')的数据挂载上,最后要把这个data作为方法的返回值,这个返回值非常重要,从而实现也可以读取数据的功能。
//有可能是获取数据,所以开始调整位置
thisCache = cache[id];
//调整返回值的位置
if (!pvt) {
//如果不是内部数据(即用户自定义数据),则挂到jQuery.chche.data上
if (!thisCache.data) {
thisCache.data = {};
} thisCache = thisCache.data;
}
/*
如果是这样调用的:$(Element).data('name','value');
那么刚好利用上面的thisCache(当前指向要挂载的空间)
把数据挂上去
*/
if (data !== undefined) {
thisCache[jQuery.camelCase(name)] = data;
}
//数据全部挂好,调整返回值
if (typeof name === "string") {
//如果参数是一个字符串
//则抓取这个字符串对应的数据
ret = thisCache[name]; //抓取失败,转换成驼峰再抓
if (ret == null) {
}
} else {
//如果参数是Object/Function,则直接返回
ret = thisCache;
} //最终返回
return ret;
internalRemoveData() - 移除数据
数据移除方法移除数据方便比较简单,但是当仓库中没有相应Element存储的数据的时候,会直接从仓库中删除这个存储空间(下面有分步分析):
function internalRemoveData(elem, name, pvt) {
//移除一个data到jQuery缓存中
if (!jQuery.acceptData(elem)) {
return;
} var thisCache, i,
isNode = elem.nodeType,
cache = isNode ? jQuery.cache : elem,
id = isNode ? elem[jQuery.expando] : jQuery.expando;
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 {
//jQuery.map将一个类数组转转换成真正的数组
//注意这里使用了连接,即如果删除失败则采用驼峰命名再次尝试删除,逻辑好严谨
name = name.concat(jQuery.map(name, jQuery.camelCase));
} i = name.length;
//删除缓存
while (i--) {
delete thisCache[name[i]];
} //如果是剩下的缓存中没有数据了,则完成了任务,否则继续
if (pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache)) {
return;
}
}
} //如果不是jQuery内部使用
if (!pvt) {
delete cache[id].data;// data也删除
//检测还有没有数据,还有数据则继续
if (!isEmptyDataObject(cache[id])) {
return;
}
} //如果是Element,则破坏缓存
if (isNode) {
jQuery.cleanData([elem], true);
} else if (support.deleteExpando || cache != cache.window) {
//不为window的情况下,或者可以浏览器检测可以删除window.属性,再次尝试删除
//低版本ie不允许删除window的属性
delete cache[id];
} else {
//否则,直接null
cache[id] = null;
}
}
1、和internalData()一样,拿钥匙。
//移除一个data到jQuery缓存中
if (!jQuery.acceptData(elem)) {
return;
} var thisCache, i,
isNode = elem.nodeType,
cache = isNode ? jQuery.cache : elem,
//根据jQuery标识拿钥匙
id = isNode ? elem[jQuery.expando] : jQuery.expando;
//如果找不到缓存,不再继续
if (!cache[id]) {
return;
}
2、找到仓库存储数据的位置,然后删除数据,这里充分的考虑了数据命名和Object参数的情况。
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 {
/*
这样都还拿不到,那还是按照自己的方式拿把,
也就是说jQuery支持
$(Element).removeData('name name2 name 3')
这样批量删除数据,真是被jQuery宠坏了...
*/
name = name.split(" ");
}
}
} else {
//jQuery.map将一个类数组转转换成真正的数组
//注意这里使用了连接,即如果删除失败则采用驼峰命名再次尝试删除,逻辑好严谨
name = name.concat(jQuery.map(name, jQuery.camelCase));
//'name-demo name-demo2'会转换成
//'name-demo name-demo2 nameDemo nameDemo2'
} i = name.length;
//删除缓存
while (i--) {
delete thisCache[name[i]];
}
/*
如果是剩下的缓存中没有数据了,则完成了任务,否则有不和谐的情况,要继续处理
isEmptyDataObject专门用来检测用户缓存空间是否是空Data,
如果缓存空间是这样的{ test:{'name':'value'} }(用户数据挂载的空间[data]是空的),就能通过
*/
if (pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache)) {
return;
}
}
}
3、如果数据全部删除了,那么仓库存储数据的空间也要被删除,所以接下来针对这些情况进行了处理
//如果不是jQuery内部使用
if (!pvt) {
delete cache[id].data;// 连data也删除
//检测还有没有数据,还有数据则继续
if (!isEmptyDataObject(cache[id])) {
return;
}
}
4、因为jQuery.data和jQuery.event事件系统直接挂钩,所以这里特别针对事件系统挂载的数据进行了删除处理,jQuery.cleanData方法涉及jQuery.event,所以暂不解读了。
//如果是Element,则破坏缓存
if (isNode) {
//和jQuery.event挂钩,不分析了...
jQuery.cleanData([elem], true);
} else if (support.deleteExpando || cache != cache.window) {
//不为window的情况下,或者可以浏览器检测可以删除window.属性,再次尝试删除
delete cache[id];
} else {
//否则,直接粗暴的null
cache[id] = null;
}
dataAttr()和jQuery.fn.data() - 针对HTML5的dataset和曝露API
dataAttr()是特别针对HTML5的dataset进行处理的方法,用处是读取Element上HTML5的data-*属性转换到jQuery.data中,是针对HTML5的兼容,典型的老夫就是要宠死你的方法:
function dataAttr(elem, key, data) {
//针对HTML5做一层特别处理,等下在jQuery.fn.data中和internalData()配合使用将会大放异彩 /*
注意这里的一层判定,在jQuery.fn.data调用的时候
会先调用用internalData(),然后把internalData()的返回值传递到这里,就是data
如果data为undefined,则进行HTML5处理
*/
if (data === undefined && elem.nodeType === 1) {
/*
rmultiDash = /([A-Z])/g
针对HTML5,把驼峰命名的数据转换为连字符:
dataSet转换为data-set
*/
var name = "data-" + key.replace(rmultiDash, "-$1").toLowerCase();
data = elem.getAttribute(name);//不用dataset是因为一些比较古老的手机没有被支持,楼主就被折磨过...
if (typeof data === "string") {
//各种丧心病狂的转换数据
//把不同的数据类型给转换成需要的类型
try {
data = data === "true" ? true :
data === "false" ? false :
data === "null" ? null :
//如果是数字
+data + "" === data ? +data :
//匹配json
rbrace.test(data) ? jQuery.parseJSON(data) :
data;
} catch (e) { }
//把HTML5的数据挂到jQuery中
jQuery.data(elem, key, data); } else {
data = undefined;
}
}
//返回这个data,jQuery.fn.data会调用dataAttr()并返回它的值
return data;
}
jQuery.data实现很简单......个屁啊,妈蛋啊看起来就是调用internalData()实现,实际上jQuery.fn.data更加的健壮,同时将各种内层的方法都联接的惟妙惟肖,当然这也意味着性能更逊色一点,
jQuery.fn.extend({
data: function (key, value) {
var i, name, data,
elem = this[0],
attrs = elem && elem.attributes;
//$(Element).data() - 获取全部数据
if (key === undefined) {
//获取
if (this.length) {
data = jQuery.data(elem);
//如果没有标志parsedAttrs的数据,则表示没有进行过HTML5的属性转换
if (elem.nodeType === 1 && !jQuery._data(elem, "parsedAttrs")) {
i = attrs.length;
while (i--) {
//那么转换HTML5的属性
if (attrs[i]) {
name = attrs[i].name;
if (name.indexOf("data-") === 0) {
name = jQuery.camelCase(name.slice(5));
//配合dataAttr进行转换
dataAttr(elem, name, data[name]);
}
}
}
//放上属性parsedAttrs,表示HTML5转换完毕
jQuery._data(elem, "parsedAttrs", true);
}
} return data;
} //$(Element).data({ name:'linkFly',value:'hello world' });
if (typeof key === "object") {
//循环设置
return this.each(function () {
jQuery.data(this, key);
});
} return arguments.length > 1 ?
//$(Element).data('name','linkFly')
this.each(function () {
jQuery.data(this, key, value);
}) :
/*
使用jQuery.data读取数据,如果读取不到,则调用dataAttr()读取并设置一遍HTML5的数据
*/
elem ? dataAttr(elem, key, jQuery.data(elem, key)) : undefined;
}
});
1、针对获取全部数据做处理,同时在内部标识上parsedAttrs,表示这个Element已经被转换过HTML5属性了:
if (key === undefined) {
//获取
if (this.length) {
data = jQuery.data(elem);
//如果没有标志parsedAttrs的数据,则表示没有进行过HTML5的属性转换
if (elem.nodeType === 1 && !jQuery._data(elem, "parsedAttrs")) {
i = attrs.length;
while (i--) {
//那么转换HTML5的属性
if (attrs[i]) {
name = attrs[i].name;
if (name.indexOf("data-") === 0) {
name = jQuery.camelCase(name.slice(5));
//配合dataAttr进行转换
dataAttr(elem, name, data[name]);
}
}
}
//放上属性parsedAttrs,表示HTML5转换完毕
jQuery._data(elem, "parsedAttrs", true);
}
} return data;
}
2、如果不是读取全部数据,则情况要么是挂载数据,要么是读取数据,但在最后的一段代码比较不错,是internalData()和dataAttr()的配合使用针对HTML5 dataset的兼容:
if (typeof key === "object") {
//循环设置
return this.each(function () {
jQuery.data(this, key);
});
} return arguments.length > 1 ?
//$(Element).data('name','linkFly')
this.each(function () {
jQuery.data(this, key, value);
}) :
/*
$(Elment).data('name')
这里的代码很有意思:
jQuery.data(elem,key)是调用internalData(),而internalData最终会返回要挂载的数据
如果用户挂载的数据是空的,则调用dataAttr()尝试转换HTML5的数据返回并且给挂到jQuery.data
*/
elem ? dataAttr(elem, key, jQuery.data(elem, key)) : undefined;
}
这里重点照顾最后一句,它实现了:
- 读取数据:$(Element).data('demo');
- 如果读取不到,读取HTML5的dataset数据并挂载到jQuery.cache中。
如果到了这里,那么调用方式会是:$(Elment).data('name'),这时候的处理方法就是:
- jQuery.data底层是internalData(),当第三个参数为空的时候,则是读取数据
- internalData()如果读取不到数据,则调用dataAttr(),而dataAttr第三个参数为undefined的时候,则会读取HTML5的dataset,然后再调用jQuery.data()(注意不是jQuery.fn.data)再挂一次数据。
- jQuery.expando是钥匙的关键,将jQuery.expando的值挂在Element上,就好像在你身上挂了一张银行卡,而银行卡的密码,则是jQuery.guid(累加不重复)。
- 通过钥匙找到仓库,进行操作。
- internalData()的思路很值得借鉴,在挂数据的时候同时取数据,尤其在jQuery.cache这个相对比较复杂的环境里,如何更高效的取数据本身就是一件值得思考的事情。
- internalRemoveData()实现了深度删除数据,尽可能让数据仿佛从未存在过,并且尝试了多种删除。
- dataAttr()是针对HTML5特别的兼容处理。
- internalData()方法非常的严谨,但是它仍然只是为了挂载数据和移除数据而生,非常纯粹而简单的工作着,真正让jQuery健壮的是jQuery.fn.data。
jQuery.2.x中jQuery.data实现
这里的jQuery.2.x主要是指jQuery.2.1。
在jQuery.2.x中,jQuery.data终于决定被好好深造一下了,过去1.x的代码说多了都是泪,jQuery.2.x没有了兼容性的后顾之忧,改写后的代码读起来简直不要太舒适啊。
在jQuery.2.x中,为数据缓存建立了Data对象,一个Data对象表示一个数据仓库——用户数据和内部数据各自使用不同的Data对象,这样就不需要在仓库里翻来翻去的查找数据存储的位置了(jQuery.cache[钥匙]和jQuery.cache[钥匙].data),思路上,仍然和jQuery.1.x一致,采用扩展属性的方式实现,关键点在Data.prorotype.key()上。
Data对象 - 数据仓库
Data对象经过封装以后衍生了这些API:
- key:专门用来获取和放置Element的钥匙。
- set/get:放置和获取数据
- access:通用API,根据参数既然可以放置也可以获取数据
- remove:移除数据
- hasData:检测是否有数据
- discard:丢弃掉这个Element的存储空间
其他的实现都比较简单,我们需要关注钥匙这里,也就是Data.prototype.key()。
Data.prototype.key() - 钥匙
Data.prototype = {
//获取缓存的钥匙
key: function (owner) {
//检测是否可以存放钥匙
if (!Data.accepts(owner)) {
return 0; //return false
} var descriptor = {},
//获取钥匙,还是在Element上挂载jQuery属性
unlock = owner[this.expando];
//如果钥匙没有则创建
if (!unlock) {
unlock = Data.uid++;
try {
//把expando转移到Data中,没一个Data实例都有不同的expando
descriptor[this.expando] = { value: unlock };
//参考:http://msdn.microsoft.com/zh-cn/library/ie/ff800817%28v=vs.94%29.aspx
//这个属性不会被枚举
Object.defineProperties(owner, descriptor);
} catch (e) {
//如果没有Object.defineProperties,则采用jQuery.extend
descriptor[this.expando] = unlock;
jQuery.extend(owner, descriptor);
}
}
if (!this.cache[unlock]) {
this.cache[unlock] = {};
}
//返回这个钥匙
return unlock;
}
};
因为用户数据和jQuery内部数据通过Data分离,所以set/get在拿到钥匙之后都比较简单。
access() - 通用接口
在创建Data对象的时候,顺便为jQuery创建了静态方法——jQuery.access:通用的底层方法,既能设置也能读取,它应用在jQuery很多API中,例如:Text()、HTML()等。
var access = jQuery.access = function (elems, fn, key, value, chainable, emptyGet, raw) {
//元素,委托的方法,属性名,属性值,是否链式,当返回空数据的时候采用的默认值,fn参数是否是Function
//一组通用(内部)方法,既然设置也能获取Data
var i = 0,
len = elems.length,
bulk = key == null;
//Object
if (jQuery.type(key) === "object") {
//如果是放数据,Object类型,则循环执行fn
chainable = true;//这里修正了是否链式....
for (i in key) {
jQuery.access(elems, fn, i, key[i], true, emptyGet, raw);
} // Sets one value
} else if (value !== undefined) {
chainable = true;
//如果设置的value是Function
if (!jQuery.isFunction(value)) {
raw = true;
}
//当参数是这样的:access(elems,fn,null)
if (bulk) {
if (raw) {
//参数是这样的:access(elems,fn,null,function)
fn.call(elems, value);
fn = null;
} else {
//参数是这样的:access(elems,fn,null,String/Object)
bulk = fn;//这里把fn给调换了
fn = function (elem, key, value) {
//这个jQuery()封装的真是....
return bulk.call(jQuery(elem), value);
};
}
}
//到了这里如果还可以执行的话那么参数是:access(elems,fn,key,Function||Object/String)
if (fn) {
for (; i < len; i++) {
//循环每一项执行
fn(elems[i], key, raw ? value : value.call(elems[i], i, fn(elems[i], key)));
}
}
} return chainable ?
//如果是设置数据,这个elems最终被返回,而在jQuery.fn.data中这个elems是this——也就是jQuery对象,保证了链式
elems : // Gets
//如果上面的设置方法都没有走,那么就是获取
bulk ?//bulk是不同的工作模式,参阅jQuery.css,jQuery.attr
fn.call(elems) :
len ? fn(elems[0], key) : emptyGet;
};
jQuery.fn.data() - 曝露API
相比jQuery.1.x代码更加的细腻了许多,这里配合着上面定义的access()使用,为每一个循环的jQuery项设置和读取数据,阅读起来比较轻松。
jQuery.fn.extend({
data: function (key, value) {
var i, name, data,
elem = this[0],
attrs = elem && elem.attributes; // 获取全部的数据,和1.x思路一致
if (key === undefined) {
if (this.length) {
data = data_user.get(elem); if (elem.nodeType === 1 && !data_priv.get(elem, "hasDataAttrs")) {
i = attrs.length;
while (i--) { // Support: IE11+
// The attrs elements can be null (#14894)
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;
} // 设置Object类型的的数据
if (typeof key === "object") {
return this.each(function () {
data_user.set(this, key);
});
}
//调用jQuery.access
return access(this, function (value) {
//value则是挂载的数据名(即使外面挂载的Object也会被拆开到这里一个个循环执行)
var data,
camelKey = jQuery.camelCase(key);//转换驼峰
if (elem && value === undefined) {
//拿数据
data = data_user.get(elem, key);
if (data !== undefined) {
return data;
}
//用驼峰拿
data = data_user.get(elem, camelKey);
if (data !== undefined) {
return data;
}
//用HTML5拿
data = dataAttr(elem, camelKey, undefined);
if (data !== undefined) {
return data;
}
return;
}
//循环每一项设置
this.each(function () {
//提前设置驼峰的...
data_user.set(this, camelKey, value);
if (key.indexOf("-") !== -1 && data !== undefined) {
//如果有name-name命名再设一边
data_user.set(this, key, value);
}
});
}, null, value, arguments.length > 1, null, true);
}, removeData: function (key) {
return this.each(function () {
//调用相应Data实例方法移除即可
data_user.remove(this, key);
});
}
});
其他实现
这些实现都是在司徒正美的《javascript框架设计》 - "数据缓存系统"一章里读到的,有必要宣传和感谢一下这本书,了解了很多代码的由来促进了理解。
这些实现其实都是针对钥匙怎么交给Element这个问题上进行的探索。
valueOf()重写
在jQuery.2.x最初设计的jQuery.data中,作者也在为Element挂载这个expando属性作为钥匙而头疼,于是给出了另外一种钥匙的挂载方法——重写valueOf()。 Waldron
在为Element挂载钥匙的时候,不再给这个Element声明属性,而是通过重写Element的valueOf方法实现。
虽然我翻了jQuery.2.0.0 - jQuery.2.1.1都没有找到这种做法,但觉得还是有必要提一下:
function Data() {
this.cache = {};
};
Data.uid = 1;
Data.prototype = {
locker: function (owner) {
var ovalueOf,
unlock = owner.valueOf(Data);
/*
owner为元素节点、文档对象、window
传递Data类,如果返回object说明没有被重写,返回string则表示已被重写
整个过程被jQuery称之为开锁,通过valueOf得到钥匙,进入仓库
*/
if (typeof unlock !== 'string') {
//通过闭包保存,也意味着内存消耗更大
unlock = jQuery.expando + Data.uid++;
//缓存原valueOf方法
ovalueOf = owner.valueOf;
Object.defineProperty(owner, 'valueOf', {
value: function (pick) {
//传入Data
if (pick === Data)
return unlock; //返回钥匙
return ovalueOf.apply(owner); //返回原valueOf方法
}
});
}
if (!this.cache[unlock])
this.cache[unlock] = {};
return unlock;
},
get: function (owner, key) {
var cache = this.cache[this.locker(owner)];
return key === undefined ? cache : cache[key];
},
set: function (owner, key, value) {
//略
}
/*其他方法略*/
};
思路上很是新颖——因为在js中几乎所有的js数据类型(null,undefined除外)都拥有valueOf/toString方法,所以直接重写Element的valueOf,在传入Data对象的时候,返回钥匙,否则返回原valueOf方法——优点是钥匙隐性挂到了Element上,保证了Element的干净和无需再考虑挂属性兼不兼容等问题了,而缺点就是采用闭包,所以内存消耗更大,或许jQuery也觉得这种做法的内存消耗不能忍,所以仍未采用——相比较放置钥匙到Element的方式,还是后者更加的纯粹和稳定。
Array.prototype.indexOf()
Array.prototype.indexOf()是ECMAScript 5(低版本浏览器可以使用代码模拟)定义的方法——可以从一组Array中检索某项是否存在?存在返回该项索引:不存在则返回-1。听起来很相似?没错,它就是String.prototype.indexOf()的数组版。
正是因为提供了针对数组项的查找,所以可以采用新的思路:
- 1、将使用data()方法挂载数据的Element通过闭包缓存到一个数组中
- 2、当下次需要检索和这个Element关联的数据的时候,只需要通过Array.ptototype.indexOf在闭包中查找到这个数组即可,而闭包中这个数组查找到的索引,就是钥匙。
代码如下:
(function () {
var caches = [],
add = function (owner) {
/* //拆开来是这样子的
var length = caches.push(owner);//返回Array的length
return caches[length - 1] = {};//新建对象并返回
*/
return caches(caches.push(owner) - 1) = {};
},
addData = function (owner, name, data) {
var index = caches.indexOf(owner), //查找索引,索引即是钥匙
//获取仓库
cache = index === -1 ? add(owner) : caches[index];
//针对仓库放数据即可
}
//其他代码略
})();
这样就不需要在Element上挂载自定义的属性(钥匙)了——然而因为每个使用过data()的Element都会在缓存下来,那么内存的消耗必不可免,相比上一种重写valueOf重写消耗更加的不能直视,这是一个有趣但并不推荐的解决方案。
WeakMap
技术总是层出不穷的,对于目前的我们来说可望不可及的ECMAScript 6定义了新的对象——WeakMap,请参考这三篇:
WeakMap对象的键值持有其所引用对象的弱引用——当那个对象被垃圾回收销毁的时候,WeakMap对象相应的键值也会被删除。它使用get/set方法将成员添加到WeakMap——简直就是为数据缓存系统/jQuery.data量身定做的。使用它,我们Data.key()方法可以改写成下面的代码:
(function () {
var caches = new WeakMap(),//缓存中心
addData = function (owner, name, data) {
//根据Element获取相应仓库存储的空间
var cache = caches.get(owner);
//如果获取不到,则开辟空间
if (!cache) {
cache = {};
//放到WeakMap对象中
caches.set(owner, cache);
}
//挂数据
cache[name] = data;
return cache;
},
removeData = function (owner, name) {
var cache = caches.get(owner);
//name为undefined的时候返回全部data,否则返回name指定的data
return name === undefined ? cache : cache && cache[name];
}
//其他代码略
});
引用
- 手抄源码(有注释):jQuery.data.js
- 参考书籍:司徒正美 - 《javascript框架设计》第8章:数据缓存系统
如果你觉得这篇文章不错,请随手点一下右下角的“推荐”,举手之劳,却鼓舞人心,何乐而不为呢?