jQuery源碼學習(9)-數據緩存

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,講解通俗易懂):


當調用$("#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 內部的數據做 增刪改查的操作

發佈了41 篇原創文章 · 獲贊 15 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章