jQuery源码解读 - 数据缓存系统:jQuery.data
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]; } //其他代码略 });
引用
如果你觉得这篇文章不错,请随手点一下右下角的“推荐”,举手之劳,却鼓舞人心,何乐而不为呢?