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 相关文章推荐
JQuery与Ajax常用代码实现对比
Oct 03 Javascript
AngularJS表格详解及示例代码
Aug 17 Javascript
Jquery组件easyUi实现手风琴(折叠面板)示例
Aug 23 Javascript
Bootstrap 设置datetimepicker在屏幕上面弹出设置方法
Mar 21 Javascript
浅谈vue项目重构技术要点和总结
Jan 23 Javascript
基于vue.js 2.x的虚拟滚动条的示例代码
Jan 23 Javascript
jQuery 同时获取多个标签的指定内容并储存为数组
Nov 20 jQuery
原生JS forEach()和map()遍历的区别、兼容写法及jQuery $.each、$.map遍历操作
Feb 27 jQuery
JavaScript闭包相关知识解析
Oct 19 Javascript
使用Vue-cli 中为单独页面设置背景图片铺满全屏
Jul 17 Javascript
通过高德地图API获得某条道路上的所有坐标用于描绘道路的方法
Aug 24 Javascript
JavaScript手写数组的常用函数总结
Nov 22 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
农民C键的运用技巧
2020/03/04 星际争霸
PHP安全下载文件的方法
2016/04/07 PHP
CI框架文件上传类及图像处理类用法分析
2016/05/18 PHP
PHP入门教程之正则表达式基本用法实例详解(正则匹配,搜索,分割等)
2016/09/11 PHP
PHP实现动态创建XML文档的方法
2018/03/30 PHP
简单实用的PHP文本缓存类实例
2019/03/22 PHP
yii 框架实现按天,月,年,自定义时间段统计数据的方法分析
2020/04/04 PHP
为JavaScript提供睡眠功能(sleep) 自编译JS引擎
2010/08/16 Javascript
js实现上传图片之上传前预览图片
2013/03/25 Javascript
JavaScript cookie的设置获取删除详解
2014/02/11 Javascript
Nodejs学习笔记之入门篇
2015/04/16 NodeJs
Bootstrap php制作动态分页标签
2016/12/23 Javascript
JQuery animate动画应用示例
2019/05/14 jQuery
vue实现滑动切换效果(仅在手机模式下可用)
2020/06/29 Javascript
React路由鉴权的实现方法
2019/09/05 Javascript
jQuery zTree如何改变指定节点文本样式
2020/10/16 jQuery
[43:47]完美世界DOTA2联赛PWL S3 LBZS vs Phoenix 第一场 12.09
2020/12/11 DOTA
Python基于PycURL实现POST的方法
2015/07/25 Python
Python中time模块和datetime模块的用法示例
2016/02/28 Python
Python编程生成随机用户名及密码的方法示例
2017/05/05 Python
对Python闭包与延迟绑定的方法详解
2019/01/07 Python
如何运行.ipynb文件的图文讲解
2019/06/27 Python
Django缓存系统实现过程解析
2019/08/02 Python
python实现一个点绕另一个点旋转后的坐标
2019/12/04 Python
使用 Python 处理3万多条数据只要几秒钟
2020/01/19 Python
python高阶函数map()和reduce()实例解析
2020/03/16 Python
django中嵌套的try-except实例
2020/05/21 Python
python3.7调试的实例方法
2020/07/21 Python
pytorch加载语音类自定义数据集的方法教程
2020/11/10 Python
跨域修改iframe页面内容详解
2019/10/31 HTML / CSS
餐厅总厨求职信
2014/03/04 职场文书
法制宣传日活动总结
2014/04/29 职场文书
委托书的写法
2014/09/16 职场文书
给医院的感谢信
2015/01/21 职场文书
公司转让协议书
2016/03/19 职场文书
SpringBoot SpringEL表达式的使用
2021/07/25 Java/Android