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基础框架浅入剖析
Dec 27 Javascript
jQuery图片滚动图片的效果(另类实现)
Jun 02 Javascript
jQuery点击弹出下拉菜单的小例子
Aug 01 Javascript
浏览器的JavaScript引擎的识别方法
Oct 20 Javascript
分析了一下JQuery中的extend方法实现原理
Feb 27 Javascript
jQuery超精致图片轮播幻灯片特效代码分享
Sep 10 Javascript
javascript 用函数实现继承详解
May 28 Javascript
Bootstrap源码解读导航条(7)
Dec 23 Javascript
JSON中key动态设置及JSON.parse和JSON.stringify()的区别
Dec 29 Javascript
Vue.directive()的用法和实例详解
Mar 04 Javascript
微信小程序生成二维码的示例代码
Mar 29 Javascript
基于vue手写tree插件的那点事儿
Aug 20 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生成带有雪花背景的验证码
2006/10/09 PHP
繁体中文转换为简体中文的PHP函数
2006/10/09 PHP
那些年一起学习的PHP(三)
2012/03/22 PHP
又一个PHP实现的冒泡排序算法分享
2014/08/21 PHP
php调用shell的方法
2014/11/05 PHP
Yii视图操作之自定义分页实现方法
2016/07/14 PHP
Yii控制器中filter过滤器用法分析
2016/07/15 PHP
PHP yii实现model添加默认值的方法(两种方法)
2016/11/10 PHP
CI框架网页缓存简单用法分析
2018/12/26 PHP
PHP+Mysql分布式事务与解决方案深入理解
2021/02/27 PHP
Jquery中对数组的操作代码
2011/08/12 Javascript
浏览器的JavaScript引擎的识别方法
2013/10/20 Javascript
基于js与flash实现的网站flv视频播放插件代码
2014/10/14 Javascript
node.js使用npm 安装插件时提示install Error: ENOENT报错的解决方法
2014/11/20 Javascript
JS返回只包含数字类型的数组实例分析
2016/12/16 Javascript
详解AngularJS通过ocLazyLoad实现动态(懒)加载模块和依赖
2017/03/01 Javascript
Angular6 写一个简单的Select组件示例
2018/08/20 Javascript
详解javascript 变量提升(Hoisting)
2019/03/12 Javascript
微信小程序 调用微信授权窗口相关问题解决
2019/07/25 Javascript
JS函数参数的传递与同名参数实例分析
2020/03/16 Javascript
详解微信小程序入门从这里出发(登录注册、开发工具、文件及结构介绍)
2020/07/21 Javascript
vant 自定义 van-dropdown-item的用法
2020/08/05 Javascript
深入分析python中整型不会溢出问题
2018/06/18 Python
在Python中使用Neo4j的方法
2019/03/14 Python
Python实现九宫格式的朋友圈功能内附“马云”朋友圈
2019/05/07 Python
python 对字典按照value进行排序的方法
2019/05/09 Python
Python如何实现转换URL详解
2019/07/02 Python
基于python解线性矩阵方程(numpy中的matrix类)
2019/10/21 Python
HTML5 progress和meter控件_动力节点Java学院整理
2017/07/06 HTML / CSS
日本最大美瞳直送网:Morecontact(中文)
2019/04/03 全球购物
越南母婴用品购物网站:Kids Plaza
2020/04/09 全球购物
小学生九一八纪念日83周年演讲稿500字
2014/09/17 职场文书
导游词之无锡丝业博物馆
2019/11/12 职场文书
nginx实现发布静态资源的方法
2021/03/31 Servers
nginx安装以及配置的详细过程记录
2021/09/15 Servers
python语言中pandas字符串分割str.split()函数
2022/08/05 Python