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 內部的數據做 增刪改查的操作