jQuery 数据缓存模块进化史详细介绍


Posted in Javascript onNovember 19, 2012

数据缓存系统最早应该是jQuery1.2引入的,那时它的事件系统完成照搬DE大神的addEvent.js,而addEvent在实现有个缺憾,它把事件的回调都放到EventTarget之上,这会引发循环引用,如果EventTarget是window对象,又会引发全局污染。有了数据缓存系统,除了规避这两个风险外,我们还可以有效地保存不同方法产生的中间变量,而这些变量会对另一个模块的方法有用,解耦方法间的依赖。对于jQuery来说,它的事件克隆乃至后来的列队实现都是离不开缓存系统。

jQuery1.2 在core模块新增了两个静态方法, data与removeData。data不用说,与jQuery其他方法一样,读写结合。jQuery的缓存系统是把所有数据都放$.cache之上,然后为每个要使用缓存系统的元素节点,文档对象与window对象分配一个UUID。UUID的属性名为一个随机的自定义属性,"jQuery" + (new Date()).getTime(), 值为整数,从零递增。但UUID总要附于一个对象上,如果那个对象是window,岂不是全局污染吗,因此jQuery内部判定它是window对象时,映射为一个叫windowData的空对象,然后UUID加在它之上。有了UUID,我们在首次访问缓存系统时,会在$.cache对象开辟一个空对象(缓存体),用于放置与目标对象有关的东西。这有点像银行开户了,UUID的值就是存折。removeData则会删掉不再需要保存数据,如果到最后,数据删清光了,它也没有任何键值对,成为空对象,jQuery就会从$.cache中删掉此对象,并从目标对象移除UUID。

//jQuery1.2.3 
var expando = "jQuery" + (new Date()).getTime(), uuid = 0, windowData = {}; 
jQuery.extend({ 
cache: {}, 
data: function( elem, name, data ) { 
elem = elem == window ? windowData : elem;//对window对象做特别处理 
var id = elem[ expando ]; 
if ( !id ) //如果没有UUID则新设一个 
id = elem[ expando ] = ++uuid; 
//如果没有在$.cache中开户,则先开户 
if ( name && !jQuery.cache[ id ] ) 
jQuery.cache[ id ] = {}; // 第三个参数不为undefined时,为写操作 
if ( data != undefined ) 
jQuery.cache[ id ][ name ] = data; 
//如果只有一个参数,则返回缓存对象,两个参数则返回目标数据 
return name ? jQuery.cache[ id ][ name ] : id; 
}, 
removeData: function( elem, name ) { 
elem = elem == window ? windowData : elem; 
var id = elem[ expando ]; 
if ( name ) {//移除目标数据 
if ( jQuery.cache[ id ] ) { 
delete jQuery.cache[ id ][ name ]; 
name = ""; 
for ( name in jQuery.cache[ id ] ) 
break; 
//遍历缓存体,如果不为空,那name会被改写,如果没有被改写,则!name 为true, 
//从而引发再次调用此方法,但这次是只传一个参数,移除缓存体, 
if ( !name ) 
jQuery.removeData( elem ); 
} 
} else { 
//移除UUID,但IE下对元素使用delete会抛错 
try { 
delete elem[ expando ]; 
} catch(e){ 
if ( elem.removeAttribute ) 
elem.removeAttribute( expando ); 
}//注销账户 
delete jQuery.cache[ id ]; 
} 
} 
})

jQuery在1.2.3中添加了两个同名的原型方法data与removeData,目的是方便链式操作与集化操作。并在data中添加getData, setData的自定义事件的触发逻辑。

1.3中,数据缓存系统终于独立成一个模块data.js(内部开发时的划分),并添加了两组方法,命名空间上的queue与dequeue,原型上的queue与dequeue。queue的目的很明显,就是缓存一组数据,为动画模块服务。dequeue是从一组数据中删掉一个。

//jQuery1.3 
jQuery.extend({ 

 queue: function( elem, type, data ) { 

 if ( elem ){ 

 type = (type || "fx") + "queue"; 

 var q = jQuery.data( elem, type ); 

 if ( !q || jQuery.isArray(data) )//确保储存的是一个数组 

 q = jQuery.data( elem, type, jQuery.makeArray(data) ); 

 else if( data )//然后往这个数据加东西 

 q.push( data ); 

 } 

 return q; 

 }, 

 dequeue: function( elem, type ){ 

 var queue = jQuery.queue( elem, type ), 

 fn = queue.shift();//然后删掉一个,早期它是放置动画的回调,删掉它就call一下, 

 // 但没有做是否为函数的判定,估计也没有写到文档中,为内部使用 

 if( !type || type === "fx" ) 

 fn = queue[0]; 

 if( fn !== undefined ) 

 fn.call(elem); 

 } 

})

fx模块animate方法的调用示例:

//each是并行处理多个动画,queue是一个接一个处理多个动画 
this[ optall.queue === false ? "each" : "queue" ](function(){ /*略*/})

在元素上添加自定义属性,还会引发一个问题。如果我们对这个元素进行拷贝,就会将此属性也会复制过去,导致两个元素都有相同的UUID值,出现数据被错误操作的情况。jQuery早期的复制节点实现非常简单,如果元素的cloneNode方法不会复制事件就使用cloneNode,否则使用元素的outerHTML,或父节点的innerHTML,用clean方法解析一个新元素出来。但outerHTML与innerHTML都会显式属性写在里面,因此需要用正则把它们清除掉。
//jQuery1.3.2 core.js clone方法 

var ret = this.map(function(){ 

 if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) { 

 var html = this.outerHTML; 

 if ( !html ) { 

 var div = this.ownerDocument.createElement("div"); 

 div.appendChild( this.cloneNode(true) ); 

 html = div.innerHTML; 

 } 

 

 return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0]; 

 } else 

 return this.cloneNode(true); 

});

jQuery1.4发现IE如果对于object, ember, applet这三个古老的用于接入外部资源的标签可能会抛错。由于旧式IE的元素节点只是COM的包装,一旦引入资源后,它就会变成那种资源的实例,而它们会有严格的访问控制,不能像普通的JS对象那样随意添加成员。于是jQuery便一刀换,但凡是这三种标签,就不为它缓存数据。jQuery弄了一个叫noData的hash,用于检测元素节点的标签。
noData: { 
 "embed": true, 

 "object": true, 

 "applet": true
}, 

//代码防御 

if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) { 

 return; 

}

jQuery1.4还对$.data进行改进,允许第二个参数为对象,方便储存多个数据。UUID对应的自定义属性expando 也放进命名空间之下了。queue与dequeue方法被剥离成一个新模块。
jQuery1.43带来三项改进。
首先是添加changeData自定义方法。不过这套方法没有什么销量,只是产品经理的自恋吧。
检测元素节点是否支持添加自定义属性的逻辑被独立成一个叫acceptData的方法。因为jQuery团队发现当object标签加载的flash资源,它还是可以添加自定义属性的,于是决定对这种情况网开一面。IE在加载flash时,需要对object指定一个叫classId的属性,值为clsid:D27CDB6E-AE6D-11cf-96B8-444553540000,因此检测逻辑就变得非常复杂,由于data, removeData都要用到,独立出来有效节省比特。
HTML5对人们随便添加自定义属性的行为做出回应,新增一种叫"data-*"的缓存机制。当用户设置的属性以"data-"开头,它们会被保存到元素节点的dataset对象上。这就导致人们可能用HTML5方便缓存数据,也可能用jQuery的缓存系统保存数据,那么data方法就变得有点不中用了。于是jQuery在原型上的data做了增强,当用户第一次访问此元素节点,会遍历它所有"data-"开头的自定义属性(为了照顾旧式IE,不能直接遍历dataset),把它们放到jQuery的缓存体中。那么当用户取数据时,会先从缓存系统中,没有再使用setAttribute访问"data-"自定义属性。但HTML5的缓存系统非常弱,只能保存字符串(这当然是出于循环引用的考量),于是jQuery会将它们还原为各种数据类型,如"null",, "false", "true"变成null, false, true, 符合数字格式的字符串会转换成数字,如果它是以"{"开头"}"结尾则尝试转成一个对象。
//jQuery1.43 $.fn.data 

rbrace = /^(?:\{.*\}|\[.*\])$/; 

if ( data === undefined && this.length ) { 

 data = jQuery.data( this[0], key ); 

 if ( data === undefined && this[0].nodeType === 1 ) { 

 data = this[0].getAttribute( "data-" + key ); 

 

 if ( typeof data === "string" ) { 

 try { 

 data = data === "true" ? true : 

 data === "false" ? false : 

 data === "null" ? null : 

 !jQuery.isNaN( data ) ? parseFloat( data ) : 

 rbrace.test( data ) ? jQuery.parseJSON( data ) : 

 data; 

 } catch( e ) {} 

 

 } else { 

 data = undefined; 

 } 

 } 

}
jQuery1.5也带来三项改进。当时jQuery已经在1.42打败Prototype.js,如日中天,马太效应,用户量暴增。它的重点改为提升性能,进入fix bug阶段(用户多,相当于免费的测试员就越多,测试覆盖面就越大)。
改进expando,原来是基于时间截,现在是版本号加随机数。因此用户可能在一个页面引入多个版本的jQuery。
是否有此数据的逻辑被抽出成一个hasData方法,处理HTML5的"data-*"属性也被抽出成一个私有方法dataAttr。它们都是为了逻辑显得更清晰。dataAttr使用JSON.parse,由于这个JSON可能是JSON2.js引入的,而JSON2.js有个非常糟糕的地方,就是为一系列原生类型添加了toJSON方法,导致for in 循环判定是否为空对象出错。jQuery被逼搞了个isEmptyDataObject方法做处理。
jQuery的数据缓存系统本来就是为事件系统服务而分化出来的,到后来,它是内部众多模块的基础设施。换言之,它内部会储存许多框架用户的变量(系统数据),但一旦它公开到文档中,用户也会使用data保存他们务业中使用的数据(用户数据)。以前,用户小,变量名冲突的可能性比较少,加之jQuery为这些系统数据精挑了一些不常用的名字,__class__, __change__或加个后缀什么的,没有收到什么投诉。当jQuery成为世界级的著名框架后,用户数据名干掉系统数据名,导致事件系统或其他什么模块瘫痪就时有发生。jQuery开始对缓存体进行改造,原来就是一个对象,什么数据都往里面抛。现在它就这个缓存体内开辟一个子对象,键名为随机的jQuery.expando值,如果是系统数据就存到里面去。但events系统数据为了向前兼容起见,还是直接放到缓存体之上。至于,如何区分是系统数据,非常简单,直接在data方法添加第四个参数,真值时为系统数据。removeData时也相应提供第三个参数,用于删除系统数据。还新设了一个_data方法,专门用于操作系统数据。下面就是缓存体的结构图:
 
  
var cache = { 
jQuery14312343254:{/*放置系统数据*/} 
events: {/"放置事件名与它对应的回调列表"/} 
/*这里放置用户数据*/ 
}

jQuery1.7对缓存体做了改进,系统变量变放置data对象中,为此判定缓存体为空也要做相应的改进,现在要跳过toJSON与data。新结构如下:
var cache = { 
data:{/*放置用户数据*/} 
/*这里放置系统数据*/ 
}

jQuery1.8曾添加一个叫deleteIds的数组,用于重用UUID,但昙花一现。UUID的值从1.8起不用jQuery.uuid的了,改用jQuery.guid递增生成。重大的改进在jQuery1.83后,操作数据的实现被抽出为私有方法,命名空间与原型上的方法只是一个代理,并分成两组方法,操作用户数据的data, removeData,操作系统数据的_data,_removeData。现在光是缓存系统就是一个庞大家族了。
jQuery 数据缓存模块进化史详细介绍 
说到底,数据缓存就是在目标对象与缓存体间建立一对一的关系,然后在缓存体上操作数据,复杂度都集在前者。而在一个普通JS对象进行增删改查某属性从来没有难度,用户怎么也玩不出花招。从软件设计原则上看,这也是最好的结果(吻合KISS原则与职责单一则)。
Javascript 相关文章推荐
javascript jq 弹出层实例
Aug 25 Javascript
js判断数据类型如判断是否为数组是否为字符串等等
Jan 15 Javascript
js中setTimeout()与clearTimeout()用法实例浅析
May 12 Javascript
JS实现的简洁纵向滑动菜单(滑动门)效果
Oct 19 Javascript
手机端 HTML5使用photoswipe.js仿微信朋友圈图片放大效果
Aug 25 Javascript
jQuery css() 方法动态修改CSS属性
Sep 25 Javascript
Bootstrap 模态框实例插件案例分析
Dec 28 Javascript
jQuery实现Select下拉列表进行状态选择功能
Mar 30 jQuery
View.post() 不靠谱的地方你知道多少
Aug 29 Javascript
Vue 使用中的小技巧
Apr 26 Javascript
微信小程序 行的删除和增加操作实现详解
Sep 29 Javascript
vue实现导航菜单和编辑文本的示例代码
Jul 04 Javascript
基于jquery库的tab新形式使用
Nov 16 #Javascript
jquery getScript动态加载JS方法改进详解
Nov 15 #Javascript
javascript 图片裁剪技巧解读
Nov 15 #Javascript
中国地区三级联动下拉菜单效果分析
Nov 15 #Javascript
JavaScript 模式之工厂模式(Factory)应用介绍
Nov 15 #Javascript
解决火狐浏览器下JS setTimeout函数不兼容失效不执行的方法
Nov 14 #Javascript
ko knockoutjs动态属性绑定技巧应用
Nov 14 #Javascript
You might like
PHP curl模拟登录带验证码的网站
2015/11/30 PHP
PHPUnit测试私有属性和方法功能示例
2018/06/12 PHP
js 匿名调用实现代码
2009/06/19 Javascript
javascript textContent与innerText的异同分析
2010/10/22 Javascript
基于JQUERY的两个ListBox子项互相调整的实现代码
2011/05/07 Javascript
js实现图片拖动改变顺序附图
2014/05/13 Javascript
最精简的JavaScript实现鼠标拖动效果的方法
2015/05/11 Javascript
使用jQuery在对象中缓存选择器的简单方法
2015/06/30 Javascript
JS+CSS实现自适应选项卡宽度的圆角滑动门效果
2015/09/15 Javascript
jQuery中的siblings用法实例分析
2015/12/24 Javascript
JavaScript判断是否是微信浏览器
2016/06/13 Javascript
jQuery AJAX timeout 超时问题详解
2016/06/21 Javascript
ES6中参数的默认值语法介绍
2017/05/03 Javascript
js阻止默认右键的下拉菜单方法
2018/01/02 Javascript
vue2.0 实现页面导航提示引导的方法
2018/03/13 Javascript
小程序实现带年月选取效果的日历
2018/06/27 Javascript
[01:29]2014DOTA2展望TI 剑指西雅图DK战队专访
2014/06/30 DOTA
python的三目运算符和not in运算符使用示例
2014/03/03 Python
Python中装饰器的一个妙用
2015/02/08 Python
python实现桌面壁纸切换功能
2019/01/21 Python
深入浅析python的第三方库pandas
2020/02/13 Python
python实现PCA降维的示例详解
2020/02/24 Python
Python logging模块原理解析及应用
2020/08/13 Python
python环境搭建和pycharm的安装配置及汉化详细教程(零基础小白版)
2020/08/19 Python
基于python tkinter的点名小程序功能的实例代码
2020/08/22 Python
Python并发爬虫常用实现方法解析
2020/11/19 Python
SmartBuyGlasses英国:购买太阳镜和眼镜
2018/01/29 全球购物
解释i节点在文件系统中的作用
2013/11/26 面试题
纠风工作实施方案
2014/03/15 职场文书
演讲主持词
2014/03/18 职场文书
合作经营协议书
2014/04/17 职场文书
员工三分钟演讲稿
2014/08/19 职场文书
感恩祖国演讲稿
2014/09/09 职场文书
预备党员群众路线教育实践活动思想汇报2014
2014/10/25 职场文书
文员岗位职责
2015/02/04 职场文书
Html5调用企业微信的实现
2021/04/16 HTML / CSS