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 相关文章推荐
js或css文件后面跟参数的原因说明
Jan 09 Javascript
ajax异步刷新实现更新数据库
Dec 03 Javascript
json格式的时间显示为正常年月日的方法
Sep 08 Javascript
setInterval()和setTimeout()的用法和区别示例介绍
Nov 17 Javascript
jquery渐隐渐显的图片幻灯闪烁切换实现方法
Feb 26 Javascript
javascript控制台详解
Jun 25 Javascript
微信小程序 底部导航栏目开发资料
Dec 05 Javascript
BootStrap学习系列之布局组件(下拉,按钮组[toolbar],上拉)
Jan 03 Javascript
从零开始学习搭建React脚手架项目
Aug 23 Javascript
mpvue 页面预加载新增preLoad生命周期的两种方式
Oct 17 Javascript
vscode 调试 node.js的方法步骤
Sep 15 Javascript
javascript对象3个属性特征
Nov 17 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/12/13 PHP
PHP 图像尺寸调整代码
2010/05/26 PHP
php json_encode奇怪问题说明
2011/09/27 PHP
php中的Base62类(适用于数值转字符串)
2013/08/12 PHP
PHP使用file_get_content设置头信息的方法
2016/02/14 PHP
页面版文本框智能提示JS代码
2009/11/20 Javascript
JS 类型转换常见方法小结
2010/05/31 Javascript
JS 按钮点击触发(兼容IE、火狐)
2013/08/07 Javascript
一个JavaScript递归实现反转数组字符串的实例
2014/10/14 Javascript
jquery实现submit提交表单
2015/02/03 Javascript
jquery实现右键菜单插件
2015/03/29 Javascript
Ajax清除浏览器js、css、图片缓存的方法
2015/08/06 Javascript
jquery插件uploadify实现带进度条的文件批量上传
2015/12/13 Javascript
javascript实现不同颜色Tab标签切换效果
2016/04/27 Javascript
ionic js 复选框 与普通的 HTML 复选框到底有没区别
2016/06/06 Javascript
纯JavaScript手写图片轮播代码
2016/10/20 Javascript
利用jQuery插件imgAreaSelect实现图片上传裁剪(放大缩小)
2016/12/02 Javascript
谈谈JavaScript中浏览器兼容问题的写法小议
2016/12/17 Javascript
图解Javascript——作用域、作用域链、闭包
2017/03/21 Javascript
vue使用better-scroll实现下拉刷新、上拉加载
2018/11/23 Javascript
详解ES7 Decorator 入门解析
2019/02/18 Javascript
layer的prompt弹出框,点击回车,触发确定事件的方法
2019/09/06 Javascript
element 中 el-menu 组件的无限极循环思路代码详解
2020/04/26 Javascript
安装dbus-python的简要教程
2015/05/05 Python
Python安装第三方库的3种方法
2015/06/21 Python
利用Python暴力破解zip文件口令的方法详解
2017/12/21 Python
Python输出\u编码将其转换成中文的实例
2018/12/15 Python
python加密解密库cryptography使用openSSL生成的密匙加密解密
2020/02/11 Python
瑞典时尚服装购物网站:Miinto.se
2017/10/30 全球购物
NFL欧洲商店(德国):NFL Europe Shop DE
2018/11/03 全球购物
英国健身仓库:Bodybuilding Warehouse
2019/03/06 全球购物
下述程序的作用是计算机数组中的最大元素值及其下标
2012/11/26 面试题
2014年清明节寄语
2014/04/03 职场文书
《去年的树》教学反思
2014/04/11 职场文书
企业年检委托书范本
2014/10/14 职场文书
圣诞晚会主持词开场白
2015/05/28 职场文书