[toc]
## 數據緩存
jQuery 最初以便捷 DOM 操作而流行,而 DOM 的本質其實就是對象,開發者們又習慣性的將一些標志直接扔給 DOM 本事,這會帶來內存泄漏的問題。
比如對于斐波那契數列,方法可以有遞歸,有迭代,如果用 js 來寫的話有一個比較有意思的方法,就是用緩存來實現:
```
function fib(n){
if(n == 1 || n == 0)
return n;
if(!fib[n-1]){
fib[n-1] = fib(n-1);
}
if(!fib[n-2]){
fib[n-2] = fib(n-2);
}
return fib[n-1] + fib[n-2];
}
```
因為 fib 不僅是函數,而且是對象,JS 中萬物都是對象,所以才有了這種緩存的解決辦法,這就是前面所說的,不過是在 DOM 上實現的。
當然這種方法也會有弊端,造成內存泄漏。現代的瀏覽器有自動回收內存的機制,但當出現循環引用或閉包的時候,就會產生內存泄漏問題。
就不深入去討論了。
## jQuery 的緩存機制
來看看 jQuery 中提高的數據緩存機制,有兩個函數,分別是 jQuery.data()和 jQuery.fn.data(),可以看出來,一個是在 jQuery 對象上,一個是在 jQuery 生成的對象上。如果仔細閱讀的話,你會發現 jQuery 中很多函數都有兩個,原型上一個,jQuery 上一個。
jQuery.data() 有兩種使用,一個用于綁定,一個用于查詢:
- jQuery.data( element, key, value )
- jQuery.data( element, key )
上面的 element 參數表示 DOM 元素,比如一個例子如下:
```
jQuery.data(document.body, 'foo', 52);
jQuery.data(document.body, 'bar', 'test');
jQuery.data(document.body, 'foo'); // 52
jQuery.data(document.body, 'bar'); // "test"
```
還有 .data() 方法,.data(),這個函數就直接在 jquery 對象上實行綁定 data:
```
$("body").data("foo", 52);
$("body").data("bar", { myType: "test", count: 40 });
$("body").data({ baz: [ 1, 2, 3 ] });
$("body").data("foo"); // 52
$("body").data(); // { foo: 52, bar: { myType: "test", count: 40 }, baz: [ 1, 2, 3 ] }
```
這邊有一個小細節數據緩存接口:
```
var jq1 = $("body");
var jq2 = $("body");
jq1.data('a', 1);
jq2.data('a', 2);
jq1.data('a'); //2
jq2.data('a'); //2
// 數據被覆蓋
$.data(jq1, 'b', 3);
$.data(jq2, 'b', 4);
$.data(jq1, 'b'); //3
$.data(jq2, 'b'); //4
// 不會被覆蓋
```
可以看出來,通過這兩種方法綁定的數據,其實是不一樣的,前者會被覆蓋,而后者不會,說明在 cache 中肯定有某種神秘的力量將他們區別開來。
## 源碼
在 jQuery 中的源碼,大致是這樣的結構:
```
function Data(){...}
Data.prototype = {
cache: function(){...},
set: function(){...},
get: function(){...},
access: function(){...},
remove: function(){...},
hasData: function(){...}
}
var dataUser = new Data();
jQuery.extend({
data: function( elem, name, data ) {
return dataUser.access( elem, name, data );
},
hasData: function( elem ) {
return dataUser.hasData( elem ) || dataPriv.hasData( elem );
},
removeData: function( elem, name ) {
dataUser.remove( elem, name );
}
})
jQuery.fn.extend({
data: function(){
...
dataUser...
...
},
removeData: function(){...}
})
```
由于之前已經弄懂 jQuery 內部結構,對于這個一點也不驚訝,在 jQuery 和 jQuery 的原型上分別有一個 data 函數,用來處理各自的情況。
既然已經知道了 data 的基本結構,我們來各個擊破,先來看一下 function Data():
```
function Data() {
// jQuery.expando 是 jQuery 的標識
this.expando = jQuery.expando + Data.uid++;
}
Data.uid = 1;
jQuery.expando = ('3.1.1' + Math.random()).replace( /\D/g, "" )
// "3.1.10.9610206515567563".replace( /\D/g, "" )
// "31109610206515567563"
```
接著是 prototype:
```
Data.prototype = {
// 建立一個 cache
cache: function( owner ) {
// Check if the owner object already has a cache
var value = owner[ this.expando ];
// If not, create one
if ( !value ) {
value = {};
// We can accept data for non-element nodes in modern browsers,
// but we should not, see #8335.
// Always return an empty object.
if ( acceptData( owner ) ) {
// 判斷 owner 是一個合格者后
if ( owner.nodeType ) {
owner[ this.expando ] = value;
// Otherwise secure it in a non-enumerable property
// configurable must be true to allow the property to be
// deleted when data is removed
} else {
Object.defineProperty( owner, this.expando, {
value: value,
configurable: true
} );
}
}
}
return value;
},
// set 函數就是為 dom 設置 key,value
set: function( owner, data, value ) {
var prop,
cache = this.cache( owner );
if ( typeof data === "string" ) {
cache[ jQuery.camelCase( data ) ] = value;
// 處理 data 為這種情況: [ owner, { properties } ]
} else {
// Copy the properties one-by-one to the cache object
for ( prop in data ) {
cache[ jQuery.camelCase( prop ) ] = data[ prop ];
}
}
return cache;
},
get: function( owner, key ) {
return key === undefined ?
this.cache( owner ) :
// Always use camelCase key (gh-2257)
owner[ this.expando ] && owner[ this.expando ][ jQuery.camelCase( key ) ];
},
// 用來訪問,將 get、set 結合到一起,并對 underfined 判斷
access: function( owner, key, value ) {
if ( key === undefined ||
( ( key && typeof key === "string" ) && value === undefined ) ) {
return this.get( owner, key );
}
this.set( owner, key, value );
return value !== undefined ? value : key;
},
// 用于移除 cache
remove: function( owner, key ) {
var i,
cache = owner[ this.expando ];
if ( cache === undefined ) {
return;
}
if ( key !== undefined ) {
// 支持刪除數組格式的 key
if ( jQuery.isArray( key ) ) {
key = key.map( jQuery.camelCase );
} else {
key = jQuery.camelCase( key );
// 為了保持一致,強行的構造了一個 數組
key = key in cache ?
[ key ] :
( key.match( rnothtmlwhite ) || [] );
}
i = key.length;
// 刪
while ( i-- ) {
delete cache[ key[ i ] ];
}
}
// cache 為空的時候,刪除整個緩存
if ( key === undefined || jQuery.isEmptyObject( cache ) ) {
if ( owner.nodeType ) {
owner[ this.expando ] = undefined;
} else {
delete owner[ this.expando ];
}
}
},
hasData: function( owner ) {
var cache = owner[ this.expando ];
return cache !== undefined && !jQuery.isEmptyObject( cache );
}
};
```
然后是 jQuery.data():
```
var dataPriv = new Data(); //以后會講到
var dataUser = new Data();
jQuery.extend( {
hasData: function( elem ) {
return dataUser.hasData( elem ) || dataPriv.hasData( elem );
},
data: function( elem, name, data ) {
return dataUser.access( elem, name, data );
},
removeData: function( elem, name ) {
dataUser.remove( elem, name );
},
// TODO: Now that all calls to _data and _removeData have been replaced
// with direct calls to dataPriv methods, these can be deprecated.
_data: function( elem, name, data ) {
return dataPriv.access( elem, name, data );
},
_removeData: function( elem, name ) {
dataPriv.remove( elem, name );
}
} );
```
源碼里面有 dataPriv 和 dataUser,作者做了一個 TODO 標記,
接著是 jQuery.fn.data():
```
jQuery.fn.extend( {
data: function( key, value ) {
var i, name, data,
// 將第一個 dom 賦給 elem
elem = this[ 0 ],
attrs = elem && elem.attributes;
// key 為 underfined,表示參數空,獲取全部
if ( key === undefined ) {
if ( this.length ) {
data = dataUser.get( elem );
if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) {
i = attrs.length;
while ( i-- ) {
// 這里面從 dom 的 attribute 中搜索 data- 開通的屬性
if ( attrs[ i ] ) {
name = attrs[ i ].name;
if ( name.indexOf( "data-" ) === 0 ) {
name = jQuery.camelCase( name.slice( 5 ) );
dataAttr( elem, name, data[ name ] );
}
}
}
dataPriv.set( elem, "hasDataAttrs", true );
}
}
return data;
}
// object 類型
if ( typeof key === "object" ) {
return this.each( function() {
dataUser.set( this, key );
} );
}
// key value 的情況,利用 access 函數
return access( this, function( value ) {
var data;
// The calling jQuery object (element matches) is not empty
// (and therefore has an element appears at this[ 0 ]) and the
// `value` parameter was not undefined. An empty jQuery object
// will result in `undefined` for elem = this[ 0 ] which will
// throw an exception if an attempt to read a data cache is made.
if ( elem && value === undefined ) {
// Attempt to get data from the cache
// The key will always be camelCased in Data
data = dataUser.get( elem, key );
if ( data !== undefined ) {
return data;
}
// Attempt to "discover" the data in
// HTML5 custom data-* attrs
data = dataAttr( elem, key );
if ( data !== undefined ) {
return data;
}
// We tried really hard, but the data doesn't exist.
return;
}
// Set the data...
this.each( function() {
// We always store the camelCased key
dataUser.set( this, key, value );
} );
}, null, value, arguments.length > 1, null, true );
},
removeData: function( key ) {
return this.each( function() {
dataUser.remove( this, key );
} );
}
});
```
data 函數略有不同,但思路也很清晰。
### 有幾個要提一下的函數
其中,有幾個函數,也來介紹一下,acceptData:
```
var acceptData = function( owner ) {
// Accepts only:
// - Node
// - Node.ELEMENT_NODE
// - Node.DOCUMENT_NODE
// - Object
// - Any
return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );
};
```
acceptData 是判斷 owner 的類型,具體關于 nodeType,去看看這里吧。
jQuery.camelCase:
```
jQuery.camelCase = function (string) {
return string.replace(/^-ms-/, "ms-").replace(/-([a-z])/g, function (all, letter) {
return letter.toUpperCase();
});
}
```
這個函數就是做了一些特殊字符串的 replace,具體有啥用,我也不是很清楚。
isEmptyObject 是判斷一個 Object 是否為空的函數,挺有意思的,可以借鑒:
```
jQuery.isEmptyObject = function (obj) {
var name;
for (name in obj) {
return false;
}
return true;
}
```
dataAttr 是一個從 DOM 中搜索以 data- 開頭屬性的函數:
```
function dataAttr( elem, key, data ) {
var name;
// If nothing was found internally, try to fetch any
// data from the HTML5 data-* attribute
if ( data === undefined && elem.nodeType === 1 ) {
name = "data-" + key.replace( /[A-Z]/g, "-$&" ).toLowerCase();
// 利用 dom 自身的 get 操作
data = elem.getAttribute( name );
if ( typeof data === "string" ) {
try {
// 先看有沒有
data = getData( data );
} catch ( e ) {}
// Make sure we set the data so it isn't changed later
dataUser.set( elem, key, data );
} else {
data = undefined;
}
}
return data;
}
```
## 總結
jQuery 的 data 緩存從源碼來看的話,真的不是很難,而且不難發現,jQuery 緩存的實質,其實就是在內部先弄一個 Object,然后和緩存體(DOM)建立一對一的聯系,所有增刪改查的操作,都是圍繞著 jQuery 內部來的,不直接對 DOM 操作,這樣就可以避免內存泄漏。而且從源碼來看,jQuery 的緩存機制自帶清內存操作,更是錦上添花呀。