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实现禁止后退的方法
Dec 27 Javascript
jQuery 连续列表实现代码
Dec 21 Javascript
兼容主流浏览器的JS复制内容到剪贴板
Dec 12 Javascript
js实现键盘Enter键提交表单的方法
May 27 Javascript
Javascript获取随机数的实现方法
Jun 22 Javascript
JS实现数组去重方法总结(六种方法)
Jul 14 Javascript
React-router v4 路由配置方法小结
Aug 08 Javascript
浅谈angularJS的$watch失效问题的解决方案
Aug 11 Javascript
JavaScript键盘事件常见用法实例分析
Jan 03 Javascript
JS事件循环机制event loop宏任务微任务原理解析
Aug 04 Javascript
vue 如何使用递归组件
Oct 23 Javascript
antd中table展开行默认展示,且不需要前边的加号操作
Nov 02 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代码
2010/02/16 PHP
php中jpgraph类库的使用介绍
2013/08/08 PHP
在Laravel中使用DataTables插件的方法
2018/05/29 PHP
fancybox1.3.1 基于Jquery的插件在IE中图片显示问题
2010/10/01 Javascript
jQuery代码优化之基本事件
2011/11/01 Javascript
图片上传判断及预览脚本的效果实例
2013/08/07 Javascript
JavaScript中的字符串操作详解
2013/11/12 Javascript
兼容主流浏览器的iframe自适应高度js脚本
2014/01/10 Javascript
nodejs下打包模块archiver详解
2014/12/03 NodeJs
Nodejs中调用系统命令、Shell脚本和Python脚本的方法和实例
2015/01/01 NodeJs
javascript实现点击商品列表checkbox实时统计金额的方法
2015/05/15 Javascript
JS获取IMG图片高宽的简单实例
2016/05/17 Javascript
JavaScript 中对象的深拷贝
2016/12/04 Javascript
提高Web性能的前端优化技巧总结
2017/02/27 Javascript
requirejs按需加载angularjs文件实例
2017/06/08 Javascript
JavaScript方法_动力节点Java学院整理
2017/06/28 Javascript
angular 数据绑定之[]和{{}}的区别
2018/09/25 Javascript
NodeJS实现同步的方法
2019/03/02 NodeJs
JavaScript迭代器的含义及用法
2019/06/21 Javascript
[01:38]完美世界DOTA2联赛(PWL)宣传片:第一站
2020/10/26 DOTA
在Python中使用swapCase()方法转换大小写的教程
2015/05/20 Python
python3利用Dlib19.7实现人脸68个特征点标定
2018/02/26 Python
Python中elasticsearch插入和更新数据的实现方法
2018/04/01 Python
opencv python统计及绘制直方图的方法
2019/01/21 Python
VSCode Python开发环境配置的详细步骤
2019/02/22 Python
Python连接mysql方法及常用参数
2020/09/01 Python
详解基于Scrapy的IP代理池搭建
2020/09/29 Python
使用python将微信image下.dat文件解密为.png的方法
2020/11/30 Python
毕业生物理教师求职信
2013/10/17 职场文书
好家长事迹材料
2014/01/23 职场文书
致全体运动员广播稿
2014/02/01 职场文书
青年文明号创建承诺
2014/03/31 职场文书
新店开张活动方案
2014/08/24 职场文书
初中生物教学随笔
2015/08/15 职场文书
如何使用vue3打造一个物料库
2021/05/08 Vue.js
如何解决flex文本溢出问题小结
2022/07/15 HTML / CSS