jQuery源码解析(架构与依赖模块) 第四章 数据缓存
4-1 内存泄露
什么是内存泄露?
内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。在C++中,因为是手动管理内存,内存泄露是经常出现的事情。而现在流行的C#和Java等语言采用了自动垃圾回收方法管理内存,正常使用的情况下几乎不会发生内存泄露。浏览器中也是采用自动垃圾回收方法管理内存,但由于浏览器垃圾回收方法有bug,会产生内存泄露。
常见内存泄露的几种情况
1.循环引用 2.Javascript闭包 3.DOM插入
一个DOM对象被一个Javascript对象引用,与此同时又引用同一个或其它的Javascript对象,这个DOM对象可能会引发内存泄漏。这个DOM对象的引用将不会在脚本停止的时候被垃圾回收器回收。要想破坏循环引用,引用DOM元素的对象或DOM对象的引用需要被赋值为null。
其实绝大部分内存泄漏都不是由Javascript引起的,浏览器的回收机制已经做的相当好了,多数的泄漏都是由于与DOM交互而产生的。
含有DOM对象的循环引用将导致大部分当前主流浏览器内存泄露
第一种:多个对象循环引用
var a=new Object; var b=new Object; a.r=b; b.r=a;
第二种:循环引用自己
var a=new Object; a.r=a;
循环引用很常见且大部分情况下是无害的,但当参与循环引用的对象中有DOM对象或者ActiveX对象时,循环引用将导致内存泄露。
我们把例子中的任何一个new Object替换成document.getElementById或者document.createElement就会发生内存泄露了。
所以这里的总结:
☑ JS的内存泄露,无怪乎就是从DOM中remove了元素,但是依然有变量或者对象引用了该DOM对象。然后内存中无法删除。使得浏览器的内存占用居高不下。这种内存占用,随着浏览器的刷新,会自动释放。
☑ 而另外一种情况,就是循环引用,一个DOM对象和JS对象之间互相引用,这样造成的情况更严重一些,即使刷新,内存也不会减少。这就是严格意义上说的内存泄露了。
所以在平时实际应用中, 我们经常需要给元素缓存一些数据,并且这些数据往往和DOM元素紧密相关。由于DOM元素(节点)也是对象, 所以我们可以直接扩展DOM元素的属性,但是如果给DOM元素添加自定义的属性和过多的数据可能会引起内存泄漏,所以应该要尽量避免这样做。 因此更好的解决方法是使用一种低耦合的方式让DOM和缓存数据能够联系起来。
所以我们必须有一种机制,避免引用数据直接依附在DOM对象上,这样尽量避免内存泄漏的产生。jQuery的缓存系统就很好的解决了这一问题。
4-2 jQuery的缓存系统
jQuery从1.2.3版本引入数据缓存系统,主要的原因就是早期的事件系统 Dean Edwards 的 ddEvent.js代码带来的问题:
1.没有一个系统的缓存机制,它把事件的回调都放到EventTarget之上,这会引发循环引用 2.如果EventTarget是window对象,又会引发全局污染不同模块之间用不同缓存变量
一般jQuery开发,我们都喜欢便捷式的把很多属性,比如状态标志都写到dom节点中,也就是HTMLElement。
好处 :
直观,便捷。
坏处 :
1.循环引用 2.直接暴露数据,安全性? 3.增加一堆的自定义属性标签,对浏览器来说是没意的 4.取数据的时候要对HTML节点做操作
jQuery缓存系统的真正魅力在于其内部应用中,动画、事件等都有用到这个缓存系统。试想如果动画的队列都存储到各DOM元素的自定义属性中,这样虽然可以方便的访问队列数据,但也同时带来了隐患。如果给DOM元素添加自定义的属性和过多的数据可能会引起内存泄漏,所以要尽量避免这么干。
A.允许我们在DOM元素上附加任意类型的数据,避免了循环引用的内存泄漏风险 B.用于存储跟dom节点相关的数据,包括事件,动画等 C.一种低耦合的方式让DOM和缓存数据能够联系起来
对于jQuery来说,数据缓存系统本来就是为事件系统服务而分化出来的,到后来,它的事件克隆乃至后来的动画列队实现数据的存储都是离不开缓存系统,所以数据缓存也算是jQuery的一个核心基础了。
jQuery的数据缓存接口:
jQuery.data( element, key, value ) .data( )
对于jQuery.data方法,原文如下:
The jQuery.data() method allows us to attach data of any type to DOM elements in a way that is safe from circular references and therefore from memory leaks. We can set several distinct values for a single element and retrieve them later:
在jQuery的官方文档中,提示用户这jQuery.data()是一个低级的方法,应该用.data()方法来代替。$.data( element, key, value )可以对DOM元素附加任何类型的数据,但应避免循环引用而导致的内存泄漏问题。
二者都是用来在元素上存放数据也就平时所说的数据缓存,都返回jQuery对象,但是内部的处理确有本质的区别。
4-3 静态与实例方法的区别
jQuery.data(ele) 与 $(ele).data()
这两个函数都是用来在元素上存放数据,也就平时所说的数据缓存,都返回jQuery对象,初学时很容易让人混淆,尤其是给dom元素添加缓存数据时。
简单的来说:
1.jQuery.data()可以实现为dom元素或js对象添加缓存 2.$("ele").data()实是对前者的扩展,其目的是可以方便的通过选择器为多个dom元素添加缓存数据
虽然大体的意思一样,但是2个接口在处理上却有差别,也是我们容易忽视的
我们看右边的代码块
为什么通过.$("ele").data()原型方法会覆盖前面key相同的值呢?
留着这个疑问,我们在之后的源码会分析。
4-4 jQuery缓存的设计思路
jQuery缓存设计接口对数据的处理有如下几种:
用name和value为对象附加数据 一个对象为对象附加数据 为 DOM Element 附加数据
设计的思路:(请参考右边代码)
常规的数据缓存,我们都大多为了方便直接就绑定到了dom对应的元素上了,最为常见的就是事件对象的回调函数了,还有一些DOM的属性。当然这也不是不可以,jQuery早期就是这么干的,但是容易引发循环引用,也会带来一定的全局污染的问题。那么jQuery在之后的改进就独立出了一个”数据缓存“的模块。
其核心的关键就是:
数据存放在内存中,通过一个映射关系与直接的DOM元素发生关联
数据缓存,jQuery现在支持两种:
1. dom元素,数据存储在jQuery.cache中。 2. 普通js对象,数据存储在该对象中。
首先先要在内存中开辟一个区域,用来保存数据,jQuery用cache对象{},那么所有的数据无法就是针对cache的CURD操作了。
1:如果是DOM元素,通过分配一个唯一的关联id把DOM元素和该DOM元素的数据缓存对象关联起来,关联id被附加到以jQuery.expando的值命名的属性上,数据存储在全局缓存对象jQuery.cache中。在读取、设置、移除数据时,将通过关联id从全局缓存对象jQuery.cache中找到关联的数据缓存对象,然后在数据缓存对象上执行读取、设置、移除操作。
2:如果是Javascript对象,数据则直接存储在该Javascript对象的属性jQuery.expando上。在读取、设置、移除数据时,实际上是对Javascript对象的数据缓存对象执行读取、设置、移除操作。
3:为了避免jQuery内部使用的数据和用户自定义的数据发生冲突,数据缓存模块把内部数据存储在数据缓存对象上,把自定义数据存储在数据缓存对象的属性data上。
所以jQuery在数据缓存的处理抽出一个Data类出来,通过2组不同的实例,分别处理不同的处理类型:
var data_priv = new Data(); var data_user = new Data();
一个是给jQuery内部只用,比如数据对象,queue,Deferred,事件,动画缓存
另一个对象data_user是提供给开发者使用的,比如$.attr(),$.data等等.
4-5 Data类的设计
我们看看Data类是如何构建这个缓存池的:
(1)先在jQuery内部创建一个cache对象{}, 来保存缓存数据。 然后往需要进行缓存的DOM节点上扩展一个值为expando的属性
function Data() { Object.defineProperty(this.cache = {}, 0, { get: function() { return {}; } }); this.expando = jQuery.expando + Math.random(); }
注:expando的值,用于把当前数据缓存的UUID值做一个节点的属性给写入到指定的元素上形成关联桥梁,所以,所以元素本身具有这种属性的可能性很少,所以可以忽略冲突。
(2)接着把每个节点的dom[expando]的值都设为一个自增的变量id,保持全局唯一性。 这个id的值就作为cache的key用来关联DOM节点和数据。也就是说cache[id]就取到了这个节点上的所有缓存,即id就好比是打开一个房间(DOM节点)的钥匙。 而每个元素的所有缓存都被放到了一个map映射里面,这样可以同时缓存多个数据。
Data.uid = 1;
关联起dom对象与数据缓存对象的一个索引标记,换句话说,先在dom元素上找到expando对应值,也就uid,然后通过这个uid找到数据cache对象中的内。
(3)所以cache对象结构应该像下面这样:
var cache = { "uid1": { // DOM节点1缓存数据, "name1": value1, "name2": value2 }, "uid2": { // DOM节点2缓存数据, "name1": value1, "name2": value2 } // ...... };
每个uid对应一个elem缓存数据,每个缓存对象是可以由多个name value(名值对)对组成的,而value是可以是任何数据类型的。
如图如示:
流程分解:(复杂的过滤,找重的过程去掉)
第一步:jQuery本身就是包装后的数组结构,这个不需要解析了
第二步:通过data存储数据
为了不把数据与dom直接关联,所以会把数据存储到一个cache对象上 产生一个 unlock = Data.uid++; unlock 标记号 把unlock标记号,作为一个属性值 赋予$body节点 cache缓存对象中开辟一个新的空间用于存储foo数据,this.cache[ unlock ] = {}; 最后把foo数据挂到cache上,cache[ data ] = value;
第三步:通过data获取数据
从$body节点中获取到unlock标记 通过unlock在cache中取到对应的数据
流程图:
4-6 实例方法的设计
前面的第三小节留下了一个疑问:jQuery.data() 与 .data()为什么会有区别?
jQuery的方法设计大都是多用的,可以根据传递参数的个数判断是set还是get处理,不仅如此jQuery还对参数的传递类型还抽出了一个处理的方法jQuery.access,我们可以传递字符串、数组、对象等等,根据这种类型自动分解成接口所有能接受的参数。
省略了部分,比如数据的过滤,HMLT5 data的处理之类,保留直接的处理,如下代码:
jQuery.fn.extend({ data: function(key, value) { return access(this, function(value) { // 通过access解析出参数 value的值 }, null, value, arguments.length > 1, null, true) }) }
通过access解析后的参数就能让data_user接口所接收,此时我们可以调用数据对象接口开始对数据进行存储设置了。
this.each(function() { var data = data_user.get( this, camelKey ); data_user.set( this, camelKey, value ); });
因为jQuery可以是一个元素合集,所以内部需要通过each对每一个合集都遍历处理,
对数据的存储内部就是调用的data_user.get缓存类的接口。
get: function(owner, key) { var cache = this.cache[this.key(owner)]; return key === undefined ? cache : cache[key]; }
通过get方法通过key去cache中取得之前的值,如果没有则新开辟一个空间用来存储之后的新值,
通过data_user.set去设置这个新的值:
set: function(owner, data, value) { var prop, unlock = this.key(owner), cache = this.cache[unlock]; cache[data] = value; return cache; }
取出cache中对应的存储空间,然后可见
cache[ data ] = value;
数据直接就是通过对象的键值对的方式存储在内存中的。
当我们重复同一个key的时候,其实是反复操作同一个cache缓存区下的同一个key
所以当下面:
cache[‘bar’] = { myType: "慕课网一", }); cache[‘bar’] = { myType: "慕课网二", });
这种情况下,肯定是被覆盖掉了。所以也就为什么通过实例的接口会覆盖数据了。
4-7 静态接口设计
通过源码可见,静态方法是直接操作数据类的 data_user.access 方法
jQuery.extend({ data: function(elem, name, data) { return data_user.access(elem, name, data) }
静态方法data的实现不像attr操作直接把数据作为属性捆绑到元素节点上,如果为DOM Element 附加数据;DOM Element 也是一种 Object ,但 IE6、IE7 对直接附加在 DOM Element 上的对象的垃圾回收存在问题;因此我们将这些数据存放在全局缓存(我们称之为“globalCache”)中,即 “globalCache” 包含了多个 DOM Element 的 “cache”,并在 DOM Element 上添加一个属性,存放 “cache” 对应的 uid。
最后可见:
1:jQuery.data(element,[key],[value]),每一个element都会有自己的一个{key:value}对象保存着数据,所以新建的对象就算有key相同它也不会覆盖原来存在的对象key所对应的value,因为新对象保存是是在另一个{key:value}对象中
2:$("div").data("a","aaaa") 它是把数据绑定每一个匹配div节点的元素上
源码可以看出来,说到底,数据缓存就是在目标对象与缓存体间建立一对一的关系,整个Data类其实都是围绕着 thia.cache 内部的数据做增删改查的操作。