jQuery源码分析(十一): 缓存机制

HTML 2016-11-14

起步

缓存几乎是每个系统都会用到了,它能避免重复的运算,对实时性要求不高但使用频率高的就可以使用缓存。如果缓存机制没有处理好,就会造成很多弊端。

内存泄露

内存泄露是一块动态分配的内存在程序结束时还没被释放,对于其他程序来说这块内存不能被使用。

好在js运行在浏览器上,js里造成的内存泄露在浏览器进程结束时浏览器会做一些处理。现在流行的java C# python 等语言都有自动垃圾回收机制,正常情况下不会发生内存泄露,但是回收方法自身也会有bug,也会产生内存泄露,所以对浏览器也不能报太大希望。

理想情况下,js内存泄露就应该指的是脚本停止运行时没被使用又不会被垃圾回收的内存。 js常见的引起内存泄露的情况:

  1. 循环引用
  2. 闭包引用
  3. DOM插入顺序
  4. 销毁对象

循环引用

循环引用基本上是所有泄漏的始作俑者。通常情况下,脚本引擎通过垃圾回收器(GC)来处理循环引用,但是某些未知因数可能会妨碍从其环境中释放资源。

//循环引用自己
var a=new Object;
a.r=a;

//多个对象循环引用
var a=new Object();
var b=new Object();
a.r=b;
b.r=a;

如果这里不是new Object() 而是document.getElementById()等获得的dom对象。一个DOM对象被一个Javascript对象引用,与此同时又引用同一个或其它的Javascript对象,这个DOM对象可能会引发内存泄漏。

含有DOM对象的循环引用将导致大部分当前主流浏览器内存泄露。

闭包引用

由于闭包函数会使程序员在不知不觉中创建出循环引用.

var elem = document.getElementById('test'); 
elem.addEventListener('click', function() { 
    alert('You clicked ' + elem.tagName); 
    //alert('You clicked ' + this.tagName); // 不再直接引用elem变量 而应该用this才不会造成内存泄露
}); 

绑定事件的时候也会:

function bindEvent() { 
    var obj=document.createElement("XXX"); 
    obj.onclick=function(){ 
    //Even if it's a empty function 
    };
    //obj=null; //及时解绑对象的引用防止内存泄露
} 

DOM插入顺序

查了一下这是IE才有的问题,简单的来说就是在向不在DOM树上的DOM元素appendChild;IE7中,貌似为了改善内存泄露,IE7采用了极端的解决方案:离开页面时回收所有DOM树上的元素,其它一概不管。

销毁对象

a = {p: {x: 1}};
b = a.p;
delete a.p;
console.log(b.x);//1

销毁对象造成是垃圾回收器策略上的不同,已经删除的属性引用依然存在。同样的,从DOM中remove了元素,但是依然有变量或者对象引用了该DOM对象。内存中就无法删除。使得浏览器的内存占用居高不下。

引入缓存的作用

允许我们在DOM元素上附加任意类型的数据,避免了循环引用的内存泄漏风险,用于存储跟dom节点相关的数据,包括事件,动画等,一种低耦合的方式让DOM和缓存数据能够联系起来。 jq的数据缓存接口:

$.data(ele, key, value);
$().data();

使得我们可以在对象里附加数据,先看一个问题:

var a = $('#d');
var b = $('#d');
a.data('a', 1);
b.data('a', 2);
console.log(a.data('a'));//2
console.log(b.data('a'));//2

$.data(a, 'b', 1);
$.data(b, 'b', 2);
console.log($.data(a, 'b'));//1
console.log($.data(b, 'b'));//2

通过.data()方法会覆盖前面key相同的值,为什么?可以推理出来的是.data()是在dom元素上附加的,$.data是在jq对象上附加的,到底是不是这样呢。

简单的来说:$.data(obj, key, val)可以实现为obj(dom元素或js对象)添加缓存 而 $("ele").data() 是对前者的扩展,其目的是可以方便的通过选择器为多个dom元素添加缓存数据.

缓存原理

jq内部有一个Data对象来保存缓存数据。 然后往需要进行缓存的DOM节点上扩展一个值为expando的属性:

function Data() {
    // return new empty object instead with no [[set]] accessor
    Object.defineProperty( this.cache = {}, 0, {
        get: function() {
            return {};
        }
    });

    this.expando = jQuery.expando + Data.uid++;
}

这里用到了 defineProperty 的特性。它的语法是Object.defineProperty(obj, prop, descriptor)

  • obj 需要定义属性的对象。
  • prop 需定义或修改的属性的名字。
  • descriptor 将被定义或修改的属性的描述符。

直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。这种添加与prototype不同的是,这样添加的属性是不可修改不可枚举不可再次配置的。

jQuery用cache对象{}来保存所有的缓存数据,expando的值,用于把当前数据缓存的UUID值做一个节点的属性给写入到指定的元素上形成关联桥梁。

Data.uid = 1;

对于DOM元素,每个节点都有dom[expando],Data.uid++保证了expando的全局唯一性。所以当操作对象是dom元素的时候,先找到到dom的expando对应的值,也就是uid,然后通过这个uid找到cache对象中的内容。这也解释了 $('').data() 方法会覆盖前面key相同的值。所以cache的结构大致是这样的:

var cache = {
    "uid1": { // DOM节点1缓存数据,
        "name1": value1,
        "name2": value2
    },
    "uid2": { // DOM节点2缓存数据,
        "name1": value1,
        "name2": value2
    }
    // ......
};

jQuery在数据缓存的处理抽出一个Data类出来,通过2组不同的实例,分别处理不同的处理类型:

var data_priv = new Data();
var data_user = new Data();

一个是给jQuery内部只用,比如数据对象,queue,Deferred,事件,动画缓存

另一个对象data_user是提供给开发者使用的,比如$.attr(),$.data,$().data()等等.

Data原型的方法有:

Data.prototype = {
    key: function() {},
    set: function() {},
    get: function() {},
    remove: function() {},
    hasData: function() {},
    access: function() {}
}

$.data()

jQuery.extend({
    hasData: function( elem ) {
        return data_user.hasData( elem ) || data_priv.hasData( elem );
    },

    data: function( elem, name, data ) {
        return data_user.access( elem, name, data );
    },

    removeData: function( elem, name ) {
        data_user.remove( elem, name );
    },

    // TODO: Now that all calls to _data and _removeData have been replaced
    // with direct calls to data_priv methods, these can be deprecated.
    _data: function( elem, name, data ) {
        return data_priv.access( elem, name, data );
    },

    _removeData: function( elem, name ) {
        data_priv.remove( elem, name );
    }
});

可以看出,如果是 $.data(ele, key, value) 每一个ele都会有自己的{key,value}的对象保存着数据,所以新建的对象就算有key是相同的也不会覆盖原来存在的对象key对应的值,因为新对象有另一个{key:value}

$().data()

jQuery.fn.extend({
    data: function( key, value ) {
        var i, name, data,
            elem = this[ 0 ],
            attrs = elem && elem.attributes;

        //省略代码
        // Sets multiple values
        if ( typeof key === "object" ) {
            return this.each(function() {
                data_user.set( this, key );
            });
        }

        return access( this, function( value ) {
            var data,
                camelKey = jQuery.camelCase( key );
            //省略代码

            // Set the data...
            this.each(function() {
                var data = data_user.get( this, camelKey );
                data_user.set( this, camelKey, value );
                if ( key.indexOf("-") !== -1 && data !== undefined ) {
                    data_user.set( this, key, value );
                }
            });
        }, null, value, arguments.length > 1, null, true );
    },

    removeData: function( key ) {
        return this.each(function() {
            data_user.remove( this, key );
        });
    }
});

elem = this[ 0 ] 可以看出,通过 $('xxx').data() 它是把数据存在了dom节点上。

通过access解析后的参数就能让data_user接口所接收,此时我们可以调用数据对象接口开始对数据进行存储设置了。

this.each(function() {
   var data = data_user.get( this, camelKey );
   data_user.set( this, camelKey, value );
});

总结

缓存的设计思路很清晰,关键在于缓存数据的绑定是在jq对象上还是dom上。 1:$.data(ele,[key],[value]),每一个 ele 都会有自己的一个{key:value}对象保存着数据,所以新建的对象就算有key相同它也不会覆盖原来存在的对象key所对应的value,因为新对象保存是是在另一个{key:value}对象中

2:$("div").data("a","aaaa") 它是把数据绑定每一个匹配div 节点 的元素上

源码可以看出来,说到底,数据缓存就是在目标对象与缓存体间建立一对一的关系,整个Data类其实都是围绕着 this.cache 内部的数据做增删改查的操作。


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

赏个馒头吧