欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

jQuery源码学习(9)-数据缓存

程序员文章站 2024-03-04 08:18:53
...

jQuery中有三种添加数据的方法,$().attr()、$().prop()、$().data()。但是前面两种是用来在元素上添加属性的,只适合少量的数据,比如:title,class,name等。对于json这种数据量大的,就适合用data方法来添加,而data方法就是jQuery缓存机制最重要的方法。

1、历史背景:

        jQuery从1.2.3版本引入数据缓存系统,主要的原因是最初的jQuery事件系统照搬Dean Edwards的addEvent.js:将回调挂载在EventTarget上,这样下来,循环引用是不可忽视的问题,它把事件的回调都放在相应的EventTarget上,当回调中再引用EventTarget的时候,会造成循环引用。如果EventTarget是window对象,又会引发全局污染。不同模块之间用不同缓存变量,造成内存浪费。

        于是缔造了jQuery.data,在jQuery.event中通过jQuery.data挂载回调函数,这样解决了回调函数的循环引用,随时时间的推移,jQuery.data应用越来越广,例如后来的jQuery.queue

2、什么是内存泄露:

内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。举例:

var div = document.getElementById("div1");
var obj = {};
div,name = obj;
obj.age = div;
以上代码,div元素引用js对象的obj,obj引用了div元素,互相引用,导致内存泄漏。

内存泄露的几种情况

  • 循环引用
  • Javascript闭包
  • DOM插入顺序

一个DOM对象被一个Javascript对象引用,与此同时又引用同一个或其它的Javascript对象,这个DOM对象可能会引发内存泄漏。这个DOM对象的引用将不会在脚本停止的时候被垃圾回收器回收。要想破坏循环引用,引用DOM元素的对象或DOM对象的引用需要被赋值为null。

循环引用很常见且大部分情况下是无害的,但当参与循环引用的对象中有DOM对象或者ActiveX对象时,循环引用将导致内存泄露。

总结

  • JS的内存泄露,无怪乎就是从DOM中remove了元素,但是依然有变量或者对象引用了该DOM对象。然后内存中无法删除。使得浏览器的内存占用居高不下。这种内存占用,随着浏览器的刷新,会自动释放。
  • 而另外一种情况,就是循环引用,一个DOM对象和JS对象之间互相引用,这样造成的情况更严重一些,即使刷新,内存也不会减少。这就是严格意义上说的内存泄露了。

 

所以在平时实际应用中, 我们经常需要给元素缓存一些数据,并且这些数据往往和DOM元素紧密相关。由于DOM元素(节点)也是对象, 所以我们可以直接扩展DOM元素的属性,但是如果给DOM元素添加自定义的属性和过多的数据可能会引起内存泄漏,所以应该要尽量避免这样做。 因此更好的解决方法是使用一种低耦合的方式让DOM和缓存数据能够联系起来

3、jQuery引入缓存的作用

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

数据缓存接口

jQuery.data( element, key, value )

.data( )

4、data的原理

$().attr(),$().prop()这两个方法,在元素上挂载js对象时就有可能出现互相引用的问题。比如:$("#div1").attr(name,obj),obj引用div1元素,就会出现循环引用的问题。如果挂载字符串或数字就不会出现互相引用的问题。但是data方法不管挂载什么都不会出现这种情况。

data的原理(引用自博客:https://www.cnblogs.com/chaojidan/p/4179230.html,讲解通俗易懂):

jQuery源码学习(9)-数据缓存

当调用$("#div1").data("name",obj)时,会在元素div上添加一个自定义属性xxx,它的值是jQuery中一个累加的唯一值,这里是1,然后再jQuery的全局变量cache对象添加1这个属性,它的属性值是一个json,这个json对象中就会有name属性,属性值就是obj对象。当你调用$("body").data("age",obj)时,会在body元素中添加xxx属性,它的值是2,然后在cache对象中添加2这个属性,它的属性值也是一个json,这个json中就会有age这个属性,属性值就是obj。

从以上可以看出,cache中存储的name,age,以及它们的值,跟元素没有直接关系,所以不存在互相引用的现象,它们是通过一个自定义属性和自定义属性值(jQuery中唯一的number值,元素上挂载number不会出现互相引用的结果)进行关联的。

$().data('age') 在表现形式上,虽然是关联到dom上的,但是实际上处理就是在内存区开辟一个cache的缓存。

实现解析:

(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是可以是任何数据类型的。

5、data源码解析

function Data() {
			//此方法是操作js对象内部属性的
		  Object.defineProperty( this.cache = {}, 0, {  //创建cache对象
		    get: function() {      //给cache对象添加了一个0属性,它的属性值无法被修改
		      return {};
		    }
		  });

		  /*我们先来解释一下这个方法的作用,比如:var obj = {name : "hello"};obj.name = "chaojidan";
			这里是可以修改obj.name属性值的。但是如果我们在obj.name = "chaojidan"之前添加这一行代码Object.freeze(obj),
			这时obj.name还是"hello",不会改变成"chajidan"。这里的Object.defineProperty方法跟Object.freeze方法的效果是一样的。
			举个例子:var obj = {name : "hello"};Object.defineProperty(obj,0,{get: function() { return {};} }   ),
			此方法接收三个参数,第一个参数就是我们要设置的对象obj,第二个参数是属性名,也就是我们在obj对象中添加了0这个属性,
			第三个参数就是0属性的属性值。因此这时obj = { name:"hello", 0:{ get:function(){ return {};} }  };,
			然后你再obj[0] = 123; 这时obj[0]不会改变成123,而是get方法返回的值{}
			(因为0属性值json对象中只有get方法,没有set方法,get方法用来获取,set方法用来设置。如果有set方法,
                        就可以设置obj[0]的值了)。
			*/
			//唯一的一个标识,就是当你在元素节点上添加数据时,会在元素节点上添加自定义属性,
			//这个自定义属性的名字就是this.expando的值。
		  this.expando = jQuery.expando + Math.random();  
		}
		//就是一个累加的数字,也就是cache对象这边的唯一属性名。第二个元素添加数据时,它在cache对象中的属性名就是2,
		//而第一元素的属性名就是1,第三个元素的属性名就是3....
		Data.uid = 1;

		Data.accepts = function( owner ) {   
		//如果是元素节点,元素必须是Element或者Document,其他元素节点都不能添加数据到cache对象中
		  return owner.nodeType ?owner.nodeType === 1 || owner.nodeType === 9 : true;
		};

		Data.prototype = {
			//分配映射,让某元素和cache对象中的属性对象一一对应
		  key: function( owner ) {  
		    if ( !Data.accepts( owner ) ) {   
		//判断此元素是否能够把数据添加到cache中,如果能够添加,就会执行后面的代码,
		//返回一个唯一的累加的数字,就是上面的Data.uid++。
		//如果不能添加,就直接返回0。而这个0属性是不能设置数据的,只能获取。
		      return 0;
		    }

		    var descriptor = {},
		      unlock = owner[ this.expando ];  

		    if ( !unlock ) {   //如果此元素节点之前没有设置这个自定义属性值,就进入if语句设置
		      unlock = Data.uid++;  //分配一个唯一的number值也就是ID。

		      try {   
						//descriptor  = {this.expando :{value:Data.uid++}}
		        descriptor[ this.expando ] = { value: unlock };   
		        Object.defineProperties( owner, descriptor );
/*此方法的意思是,对descriptor对象中的每个属性进行defineProperty操作。
这个代码可以写成Object.defineProperty( owner, this.expando,{value:Data.uid++});
在owner这个元素节点上添加this.expando属性名(自定义属性),它的值是value的属性值Data.uid++(也就是唯一的一个number值),
并且这个属性值无法被修改,只能获得。有些浏览器不支持这个方法,所以就catch*/

		      } catch ( e ) {
		        descriptor[ this.expando ] = unlock;  
						//descriptor = {this.expando:ID}
		        jQuery.extend( owner, descriptor );
		//给元素节点添加这个自定义属性this.expando,并且设置值为ID。
		//也就是元素上会添加一个这样的属性this.expando(自定义属性) = Data.uid++(唯一的一个number值);
		      }
		     }

		    if ( !this.cache[ unlock ] ) {   
					//在cache对象中设置ID的属性名,它的属性值为{}
		      this.cache[ unlock ] = {};
		    }

		    return unlock;    
				//当对同一个元素添加第二个数据时,就会直接返回这个owner[ this.expando ]了,
				//所以同一个元素的自定义属性值是一样的。
		  },
		  set: function( owner, data, value ) {//往cache对象中添加数据值
		    var prop,
		      unlock = this.key( owner ),   //先找到这个ID(1,2,3....)
		        cache = this.cache[ unlock ];  //在cache中找这个属性名ID的json对象

		    if ( typeof data === "string" ) {
		      cache[ data ] = value;  //如果是添加字符串,就直接添加到这个json对象中

		    } else {   //如果是这种写法:$.data(document.body,{"age":30,"job":"it"})
		      if ( jQuery.isEmptyObject( cache ) ) {
		        jQuery.extend( this.cache[ unlock ], data );
		      } else {
		        for ( prop in data ) {
		          cache[ prop ] = data[ prop ];
		        }
		      }
		    }
		    return cache;
		  },
		  get: function( owner, key ) { //去cache对象中获取某个值
		    var cache = this.cache[ this.key( owner ) ];
				//如果不传入key就返回这个元素上添加的所有数据,如果传入key,就只返回key属性的属性值。
		    return key === undefined ?cache : cache[ key ];  
		  },
		  access: function( owner, key, value ) {  //对get和set进行整合,根据参数的个数决定是get还是set操作
		    var stored;
		    if ( key === undefined ||((key && typeof key === "string") && value === undefined) ) {

		      stored = this.get( owner, key );

		      return stored !== undefined ?stored : this.get( owner, jQuery.camelCase(key) );
		    }

		    this.set( owner, key, value );

		    return value !== undefined ? value : key;
		  },
		  remove: function( owner, key ) { //删除cache对象中的值
		    var i, name, camel,unlock = this.key( owner ),cache = this.cache[ unlock ];

		    if ( key === undefined ) {  //如果不传入key,就把这个元素的整个数据都删除
		      this.cache[ unlock ] = {};

		    } else { 
//如果是数组,就要删除多个属性值,比如:$.removeData(document.body,["age","job","all-name"]),
//name = ["age","job","all-name","allName"],
//map方法请参照http://www.cnblogs.com/chaojidan/p/4142338.html。		
		      if ( jQuery.isArray( key ) ) {   
		        name = key.concat( key.map( jQuery.camelCase ) );
		      } else {
		        camel = jQuery.camelCase( key );   //如果传入的就是一个值,先把这值转成驼峰形式
		        if ( key in cache ) {    //这个值是否在cache中
							//如果在,name = [key, key的驼峰写法(如果没有驼峰写法,那么就是key)]
		          name = [ key, camel ];  
		        } else {    //如果key不在cache中
						//先检查key的驼峰写法在不在cache中,如果连key的驼峰写法都不在cache中,
						//就看name是否是用空格分开的字符串,比如:"age job",那么就用正则匹配,返回[age,job]
		          name = camel;     
		          name = name in cache ?[ name ] : ( name.match( core_rnotwhite ) || [] );
		        }
		      }

		      i = name.length;
		      while ( i-- ) {     //删除cache中对应的属性值
		        delete cache[ name[ i ] ];
		      }
		    }
		  },
		  hasData: function( owner ) { //判断cache对象是否有此属性
		    return !jQuery.isEmptyObject(
		      this.cache[ owner[ this.expando ] ] || {}   //元素节点在缓存系统中是否有数据
		    );
		  },
		  discard: function( owner ) {  //一次性删除cache对象中元素节点的所有数据
		    if ( owner[ this.expando ] ) {
		      delete this.cache[ owner[ this.expando ] ];
		    }
		  }
		};

以上是Data构造方法实现的源码解析,有了这个构造方法后,我们就可以实例化Data对象,通过Data实例对象来操作jQuery缓存机制了。

实例对象Data可以调用Data原型对象中的所有方法和属性,因此只要new Data,就可以通过这个new出来的Data对象进行jQuery的缓存操作。

6、jQuery.data( element, key, value )和.data( )的区别及原理

<div id="aaron">Aron test</div>


var aa1=$("#aaron");
var aa2=$("#aaron");

//=======第一组=========
$(''
).data()方法

aa1.data('a',1111);
aa2.data('a',2222);

aa1.data('a')  //结果222222
aa2.data('a')  //结果222222

//=======第二组=========
$.data()方法

$.data(aa1,"b","1111")
$.data(aa2,"b","2222")

$.data(aa1,"b")   //结果111111
$.data(aa2,"b")   //结果222222
通过.data()方法会覆盖前面key相同的值,原因详见下面的源码解析。

jQuery的缓存系统如何使用:

实例方法使用的方式:

$("#div1").data("name","hello");   //给元素div设置name数据值

$("#div1").data("name");    //取元素div设置的name值

$("#div1").removeData("name");    //删除元素div的name数据值

静态方法使用的方式:

$.data(document.body,"age",30);  //给元素body设置age数据值

$.data(document.body,"age");  //取元素body设置的age值

$.removeData(document.body,"age");  //删除元素body的age数据值

$.hasData(document.body,"age");  //元素body是否设置了age数据值

静态方法源码解析:

data_user = new Data();    //对外使用的数据缓存对象
	data_priv = new Data();    //内部的数据缓存对象,内部使用

	jQuery.extend({      //在jQuery中添加静态方法
	  acceptData: Data.accepts,   //调用Data构造函数的accepts静态方法
        //直接调用 data_user.access 数据类的接口,传入的是elem整个jQuery对象
  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 );  },  _data: function( elem, name, data ) { //_代表私有的方法,不对外    return data_priv.access( elem, name, data );  },  _removeData: function( elem, name ) {    data_priv.remove( elem, name );  }});

静态方法,是直接调用Data原型上的方法。而在jQuery缓存系统中,我们经常使用的就是以下这几个实例方法:

jQuery.fn.extend({  //在jQuery原型上添加实例方法

	   data: function( key, value ) {   //一个参数时是获取数据,两个参数时是设置数据   

	    var attrs, name,
		//找到一组元素中的第一个,如果是获取操作,只获取一个元素,如果是设置,就设置这一组元素
	      elem = this[ 0 ],  
	        i = 0,
	          data = null;
			//当不传入参数时,就代表取这个元素的所有数据,比如:$("div").data();
	    if ( key === undefined ) {  
	      if ( this.length ) {   //如果有元素
	        data = data_user.get( elem );  //获取这个元素的数据

	//下面这一段代码是来处理HTML5中的自定义属性data-xxx的。
	//比如:<div id="div1" data-chao-ji="dan"></div>,$("#div1")[0].dataset.chaoJi等于"dan"(HTML5写法)。
	//$("#div1").data("chaoJi")等于"dan"。jQuery缓存系统会把HTML5自定义属性data-xxx这种格式的数据自动缓存起来。

			//元素是否是Element,元素是否有hasDataAttrs属性,第一次是没有的,
			//也就是undefined。data_priv是内部使用的Data对象,不会影响到外部使用的Data对象data_user。
	        if ( elem.nodeType === 1 && !data_priv.get( elem, "hasDataAttrs" ) ) {
	          attrs = elem.attributes;  //获取元素所有的属性
	          for ( ; i < attrs.length; i++ ) {
	            name = attrs[ i ].name;     //得到每一个属性的名字
				//如果元素中的属性名有data-开头的字符串,就进入到if语句
	            if ( name.indexOf( "data-" ) === 0 ) {   
				//取data-后面的字符串,也就是chao-ji,然后转驼峰格式,也就是变成chaoJi。	
	              name = jQuery.camelCase( name.slice(5) );   
				//把data-开头的属性名的值添加到缓存系统中
	              dataAttr( elem, name, data[ name ] ); 
	            }
	          }
	          data_priv.set( elem, "hasDataAttrs", true ); //设置此元素的hasDataAttrs属性值为true
	        }
	      }

	      return data;
	    }

	    if ( typeof key === "object" ) {  //比如:$("#div1").data({name:"chaojidan",age:"25"});
	      return this.each(function() {  //对每个元素,都在数据缓存中设置json中的属性值
	        data_user.set( this, key );
	      });
	    }
//access的第一个参数是所有元素,第二个参数是回调,
//第三个参数是key值,第四个参数是value值。
//第五个参数的作用是来决定回调是用来设置数据还是获取数据的(true代表设置操作,false代表获取操作)。
//第六个参数和第七个参数是内部使用的,我们不用管。
	    return jQuery.access( this, function( value ) {    
	      var data,
	        camelKey = jQuery.camelCase( key );
				//如果value为空的话,就代表参数只有一个,那么arguments。length=1 >1,返回false,
				//因此代表获取操作,获取元素elem在缓存系统中的key属性值
	      if ( elem && value === undefined ) {   
	        data = data_user.get( elem, key );   //获取数据
	        if ( data !== undefined ) {   //如果找到,就直接返回
	          return data;
	        }

	        data = data_user.get( elem, camelKey );   
	//如果没找到,就再找key的驼峰方式的属性值,比如:$("#div1").data("name-age"),
	//它会先找name-age这种属性名的值,如果没找到,就找nameAge这种属性名的值。
	        if ( data !== undefined ) {
	          return data;
	        }

	        data = dataAttr( elem, camelKey, undefined ); 
	//如果都没找到,就找HTML5自定义属性data-xxx的值,比如:元素div上的data-name-age的属性值。
	        if ( data !== undefined ) {
	          return data;
	        }

	        return;
	      }
				//区别在each方法了,处理的是每一个元素dom节点
	      this.each(function() {   //如果value存在,就代表是设置操作。就循环所有元素,对每个元素都设置
	        var data = data_user.get( this, camelKey ); //先去缓存系统中取key的驼峰形式的属性值

	        data_user.set( this, camelKey, value );   //然后把值设置给key的驼峰形式的属性名

	        if ( key.indexOf("-") !== -1 && data !== undefined ) {  
	//如果key包含"-",并且之前取到的key的驼峰形式的属性值存在,那么就把此驼峰形式的属性值,赋给key这个属性名。
	//举个例子:$("#div1").data("name-age","1"),这时缓存系统中会存储nameAge:1,
	//然后我再$("#div1").data("name-age","2"),这时data = 1,nameAge:2,进入if语句,缓存系统会再存储name-age:1。
	          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 );
	    });
	  }

	});
	//处理html5的data-*属性
	function dataAttr( elem, key, data ) {   //key=chaoJi
	  var name;
/*首先判断数据缓存中是否有此名字的属性值,如果没有,并且元素是Element,就进入if语句。
如果数据缓存中已经有了此名字的属性值,那么就直接返回这个值。
意思就是:当HTML5的data-xxx的属性名xxx与jQuery通过data方法添加到数据缓存的属性名xxx是一样的,
那么你通过data方法访问时,获取到的值是数据缓存中的xxx属性值,而不是HTML5的data-xxx的属性值,
只有数据缓存没有这个xxx的属性值时,才会去取HTML5的data-xxx的属性值。*/
	  if ( data === undefined && elem.nodeType === 1 ) {   
	    name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();  
//rmultiDash = /([A-Z])/g;取大写字母,也就是J,然后用-J代替J,这时key是chao-Ji,然后转小写,因此变成chao-ji。name= "data-chao-ji"
	    data = elem.getAttribute( name );   //得到元素上的这个HTML5自定义属性的属性值"dan"

	    if ( typeof data === "string" ) {    //如果属性值是字符串就保存到缓存系统
	      try {
	        data = data === "true" ? true :  
	          data === "false" ? false :
	            data === "null" ? null :
	              +data + "" === data ? +data :  //如果是数字字符串,就存入数字
	                rbrace.test( data ) ? JSON.parse( data ) :  //rbrace判断是否是一个json字符串,如果是就解析成json。
	                  data;   //如果是字符串,就直接返回字符串
	      } catch( e ) {}

	      data_user.set( elem, key, data );
	    } else {   
	//如果不是字符串就直接返回undefined,也就是说HTML5的data-xxx的属性值只能是一个字符串,
	//如果不是,那么你通过jQuery的data(xxx)方法获取时,是获取不到的,返回undefined。
	      data = undefined;
	    }
	  }
	  return data;
	}

源代码从源码的简单对比就很明显的看出来

  • 看jQuery.data(element,[key],[value]),每一个element都会有自己的一个{key:value}对象保存着数据,所以新建的对象就算有key相同它也不会覆盖原来存在的对象key所对应的value,因为新对象保存是是在另一个{key:value}对象中
  • $("div").data("a","aaaa") 它是把数据绑定每一个匹配div节点的元素上

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

相关标签: cache