jQuery 2.0.3 原始碼分析 資料快取
歷史背景:
jQuery從1.2.3版本引入資料快取系統,主要的原因就是早期的事件系統 Dean Edwards 的
帶來的問題:
- 沒有一個系統的快取機制,它把事件的回撥都放到EventTarget之上,這會引發迴圈引用
- 如果EventTarget是window物件,又會引發全域性汙染
- 不同模組之間用不同快取變數
一般jQuery開發,我們都喜歡便捷式的把很多屬性,比如狀態標誌都寫到dom節點中,也就是HTMLElement
好處:直觀,便捷
壞處:
- 迴圈引用
- 直接暴露資料,安全性?
- 增加一堆的自定義屬性標籤,對瀏覽器來說是沒意義的
- 取資料的時候要對HTML節點做操作
什麼是記憶體洩露
記憶體洩露是指一塊被分配的記憶體既不能使用,又不能回收,直到瀏覽器程序結束。在C++中,因為是手動管理記憶體,記憶體洩露是經常出現的事情。而現在流行的C#和Java等語言採用了自動垃圾回收方法管理記憶體,正常使用的情況下幾乎不會發生記憶體洩露。瀏覽器中也是採用自動垃圾回收方法管理記憶體,但由於瀏覽器垃圾回收方法有bug,會產生記憶體洩露。
記憶體洩露的幾種情況
- 迴圈引用
- Javascript閉包
- DOM插入順序
一個DOM物件被一個Javascript物件引用,與此同時又引用同一個或其它的Javascript物件,這個DOM物件可能會引發記憶體洩漏。這個DOM物件的引用將不會在指令碼停止的時候被垃圾回收器回收。要想破壞迴圈引用,引用DOM元素的物件或DOM物件的引用需要被賦值為null。
含有DOM物件的迴圈引用將導致大部分當前主流瀏覽器記憶體洩露
第一種:多個物件迴圈引用
var a=new Object; var b=new Object; a.r=b; b.r=a;
第二種:迴圈引用自己
var a=new Object; a.r=a;
迴圈引用很常見且大部分情況下是無害的,但當參與迴圈引用的物件中有DOM物件或者ActiveX物件時,迴圈引用將導致記憶體洩露。
我們把例子中的任何一個new Object替換成document.getElementById或者document.createElement就會發生記憶體洩露了。
具體的就深入討論了,這裡的總結
- JS的記憶體洩露,無怪乎就是從DOM中remove了元素,但是依然有變數或者物件引用了該DOM物件。然後記憶體中無法刪除。使得瀏覽器的記憶體佔用居高不下。這種記憶體佔用,隨著瀏覽器的重新整理,會自動釋放。
- 而另外一種情況,就是迴圈引用,一個DOM物件和JS物件之間互相引用,這樣造成的情況更嚴重一些,即使重新整理,記憶體也不會減少。這就是嚴格意義上說的記憶體洩露了。
所以在平時實際應用中, 我們經常需要給元素快取一些資料,並且這些資料往往和DOM元素緊密相關。由於DOM元素(節點)也是物件, 所以我們可以直接擴充套件DOM元素的屬性,但是如果給DOM元素新增自定義的屬性和過多的資料可能會引起記憶體洩漏,所以應該要儘量避免這樣做。 因此更好的解決方法是使用一種低耦合的方式讓DOM和快取資料能夠聯絡起來。
所以我們必須有一種機制,抽象出這樣的處理方式
jQuery引入快取的作用
- 允許我們在DOM元素上附加任意型別的資料,避免了迴圈引用的記憶體洩漏風險
- 用於儲存跟dom節點相關的資料,包括事件,動畫等
- 一種低耦合的方式讓DOM和快取資料能夠聯絡起來
資料快取介面
jQuery.data( element, key, value )
.data( )
對於jQuery.data方法,原文如下
The jQuery.data() method allows us to attach data of any type to DOM elements in a way that is safe from circular references and therefore from memory leaks. We can set several distinct values for a single element and retrieve them later:
在jQuery的官方文件中,提示使用者這是一個低階的方法,應該用.data()方法來代替。$.data( element, key, value )可以對DOM元素附加任何型別的資料,但應避免迴圈引用而導致的記憶體洩漏問題
都是用來在元素上存放資料也就平時所說的資料快取,都返回jQuery物件,但是內部的處理確有本質的區別
我們看一組對比
<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來說,資料快取系統本來就是為事件系統服務而分化出來的,到後來,它的事件克隆乃至後來的動畫列隊實現資料的儲存都是離不開快取系統,所以資料快取也算是jQuery的一個核心基礎了
早期jQuery的快取系統是把所有資料都放$.cache之上,然後為每個要使用快取系統的元素節點,文件物件與window物件分配一個UUID
data的實現不像attr直接把資料作為屬性捆綁到元素節點上,如果為DOM Element 附加資料;DOM Element 也是一種 Object ,但 IE6、IE7 對直接附加在 DOM Element 上的物件的垃圾回收存在問題;因此我們將這些資料存放在全域性快取(我們稱之為“globalCache”)中,即 “globalCache” 包含了多個 DOM Element 的 “cache”,並在 DOM Element 上新增一個屬性,存放 “cache” 對應的 uid
$().data('a') 在表現形式上,雖然是關聯到dom上的,但是實際上處理就是在記憶體區開闢一個cache的快取
那麼JQuery內部是如何處理,各種關聯情況與操作呢?
******************$(‘’).data()的實現方式********************
用name和value為物件附加資料
var obj = {}; $.data(obj, 'name', 'aaron'); $.data(obj,'name') //aaron
一個物件為物件附加資料
var obj = {}; $.data(obj, { name1: 'aaron1', name2: 'aaron1' }); $.data(obj) //Object {name1: "aaron1", name2: "aaron1"}
為 DOM Element 附加資料
我們用最簡單的程式碼來闡述這個處理的流程:
1.獲取節點body
var $body = $("body")
2.給body上增加一條資料,屬性為foo,值為52
$body.data("foo", 52);
3.取出foo
$body.data('foo')
考慮一個問題:
一個元素在正常情況下可以使用.remove()方法將其刪除,並清除各自的資料。但對於本地物件而言,這是不能徹底刪除的,這些相關的資料一直持續到視窗物件關閉
同樣,這些問題也存在於event 物件中,因為事件處理器(handlers)也是用該方法來儲存的。
那麼,要解決該問題最簡單的方法是將資料儲存到本地物件新增的一個屬性之中
所以如流程二解析一樣增加一個unlock標記
cache與elem 都統一起來
if ( elem.nodeType ) { cache[ id ] = dataObject; elem[ expando ] = id; } else { elem[ expando ] = dataObject; }
**************實現解析****************
(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是可以是任何資料型別的。
流程分解:(複雜的過濾,找重的過程去掉)
第一步:jQuery本身就是包裝後的陣列結構,這個不需要解析了
第二步:通過data儲存資料
- 為了把不把資料與dom直接關聯,所以會把資料儲存到一個cache物件上
- 產生一個 unlock = Data.uid++; unlock 標記號
- 把unlock標記號,作為一個屬性值 賦予$body節點
- cache快取物件中開闢一個新的空間用於儲存foo資料,this.cache[ unlock ] = {};
- 最後把foo資料掛到cache上,cache[ data ] = value;
第三步:通過data獲取資料
- 從$body節點中獲取到unlock標記
- 通過unlock在cache中取到對應的資料
流程圖:
整個過程結束,其實分解後邏輯很簡單的,只是要處理各種情況下,程式碼結構封裝就顯得很複雜了
如圖
Body元素:expando:uid
jQuery203054840829130262140.37963378243148327: 3
資料快取cache
uid:Object
那麼jQuery.data() 與 .data() 有什麼區別?
1.jQuery.data(element,[key],[value])原始碼
jQuery.extend({ acceptData: Data.accepts, hasData: function( elem ){}, //直接呼叫 data_user.access 資料類的介面,傳入的是elem整個jQuery物件 data: function( elem, name, data ) { return data_user.access( elem, name, data ); }, ........
2.data([key],[value])
jQuery.fn.extend({ data: function( elem, name, data ) { return jQuery.access( this, function( value )){ //區別在each方法了,處理的是每一個元素dom節點 this.each(function() { } } } }, ........
原始碼從原始碼的簡單對比就很明顯的看出來
- 看jQuery.data(element,[key],[value]),每一個element都會有自己的一個{key:value}物件儲存著資料,所以新建的物件就算有key相同它也不會覆蓋原來存在的物件key所對應的value,因為新物件儲存是是在另一個{key:value}物件中
- $("div").data("a","aaaa") 它是把資料繫結每一個匹配div節點的元素上
原始碼可以看出來,說到底,資料快取就是在目標物件與快取體間建立一對一的關係,整個Data類其實都是圍繞著 thia.cache 內部的資料做 增刪改查的操作
整個結構一目瞭然!
淺顯易懂的分析,讓大家都能夠理解其思路,但是實現程式碼上面確實很精妙,大家可以圍繞這個思路去看,如果有需要我可以在下篇把具體的原始碼給寫一下