jQuery源碼解讀 - 資料緩衝系統:jQuery.data

來源:互聯網
上載者:User

jQuery源碼解讀 - 資料緩衝系統:jQuery.data
jQuery在1.2後引入jQuery.data(資料緩衝系統),主要的作用是讓一組自訂的資料可以DOM元素相關聯——淺顯的說:就是讓一個對象和一組資料一對一的關聯。 一組和Element相關的資料如何關聯著這個Element一直是web前端的大姨媽,而最初的jQuery事件系統照搬Dean Edwards的addEvent.js:將回調掛載在EventTarget上,這樣下來,循環參考是不可忽視的問題。而在web前端中,資料和DOM的關係太過基情和緊張,於是jQuery在1.2中,正式締造了jQuery.data,就是為瞭解決這段孽緣:自訂資料和DOM進行關聯。 文中所說的Element主要是指資料掛載所關聯的target(目標),並不局限於Element對象。 本文原創於linkFly,原文地址。 這篇文章主要分為以下知識 jQuery.data模型jQuery.1.x中jQuery.data實現jQuery.2.x中jQuery.data實現其他實現jQuery.data模型模型凡存在,皆真理——任何一樣事物的存在必然有其存在的理由,於我們的角度來說,這叫需求。 一組資料,如何與DOM相關聯一直是web前端的痛處,因為瀏覽器的相容性等因素。最初的jQuery事件系統照搬Dean Edwards的addEvent.js:將回調掛載在EventTarget上,這樣下來,循環參考是不可忽視的問題,它把事件的回調都放在相應的EventTarget上,當回調中再引用EventTarget的時候,會造成循環參考。於是締造了jQuery.data,在jQuery.event中通過jQuery.data掛載回呼函數,這樣解決了回呼函數的循環參考,隨時時間的推移,jQuery.data應用越來越廣,例如後來的jQuery.queue。 首先我們要搞清楚jQuery.data解決的需求,有一組和DOM相關/描述Element的資料,如何存放和掛載呢?可能有人是這樣的: 使用attributesHTML: <div id="demo" userData="linkFly"></div>javascript:  (function () {            var demo = document.getElementById('demo');            console.log(demo.getAttribute('userData'));})();使用HTML5的datasetHTML: <div id="demo2" data-user="linkFly"></div>javascript:  (function () {    var demo = document.getElementById('demo2');    console.log(demo.dataset.user);})();為DOM執行個體進行擴充HTML: <div id="demo3"></div>javascript: (function () {    var demo = document.getElementById('demo3');    demo.userData = 'demo';    console.log(demo.userData);})();雖然有解決方案,但都不是理想的解決方案,每個方案都有自己的局限性:1、只能儲存字串(或轉化為字串類型)的資料,同時曝露了資料,並且在HTML上掛載了無謂的屬性,瀏覽器仍然會嘗試解析這些屬性。2、同上。3、典型的汙染,雖然可以儲存更強大的資料(Object/Function),但是患有代碼潔癖的騷年肯定是不能忍的,更主要,如果掛載的資料中引用著這個Element,則會循環參考。jQuery.data,則是為瞭解決這樣的自訂資料掛載問題。  模型一窺模型吧,jQuery.data在早期,為了相容性做了很多的事情。同時,或許是因為jQuery.data最初的需求作者也覺得太過簡單,所以實現的代碼上讓人覺得略顯倉促,早期的資料倉儲很是繁瑣,在jQuery.2.x後,jQuery.data重寫,同時終於把jQuery.data抽離出對象。 jQuery.data模型上,就是建立一個資料倉儲,而每一個掛載該資料的對象,都有自己的鑰匙,他和上面的代碼理念並不同: 上面的方案是: 在需要掛載資料的對象上掛載資料,就好像你身上一直帶著1000塊錢,要用的時候直接從口袋裡掏就可以了。 jQuery.data則是: 建立一個倉庫,所有的資料都放在這個倉庫裡,然後給每個需要掛載資料的對象一把鑰匙,讀取資料的時候拿這個鑰匙到倉庫裡去拿,就好像所有人都把錢存在銀行裡,你需要的時候則拿著銀行卡通過密碼去取錢。 圖一張:   我們暫時先不討論資料倉儲的樣子,首先我們要關注資料和Element關聯的關鍵點——鑰匙,這個鑰匙頗具爭議,後續的幾種資料緩衝方式都是在對這個鑰匙進行大的變動,因為這個鑰匙,不得不放在Element上——即使你把所有的錢都存在銀行裡了,但是你身上還是要有相應的鑰匙,這不得不讓那些代碼潔癖的童鞋面對這個問題:Element註定要被汙染——jQuery.data只是嘗試了最小的汙染。 jQuery在建立的時候,會產生一個屬性——jQuery.expando: 1expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" );jQuery.expando是當前頁面中引用的jQuery對象的身份標誌(id),每個頁面引用的jQuery.expando都是不重複且唯一的,所以這就是鑰匙的關鍵:jQuery.expando產生的值作為鑰匙,掛載在Element上,也就是為Element建立一個屬性:這個屬性的名稱,就是jQuery.expando的值,這就是鑰匙的關鍵。 雖然仍然要在Element上掛載自己的資料,但是jQuery儘可能做到了最小化影響使用者的東西。 當然這裡需要注意:通過為Element添加鑰匙的時候,使用的是jQuery.expando的值作為添加的屬性名稱,頁面每個使用過jQuery.data的Element上都有jQuery.expando的值擴充的屬性名稱,也就是說,每個使用過jQuery.data的Element都有這個擴充的屬性,通過檢索這個屬性值來找到倉庫裡的資料——鑰匙是這個屬性值,而不是這個jQuery.expando擴充的屬性名稱。 木圖木真相:   jQuery.1.x(截至jQuery.1.11)中,內部資料和外部資料掛載在jQuery.cache不同的地方——內部資料掛載在jQuery.cache[鑰匙]下,而使用者資料則掛載在jQuery.cache[鑰匙].data下,原因是因為內部資料如何是使用者資料掛載在一起則會存在相互覆蓋的情況,要把資料給隔離開。  jQuery.1.x中jQuery.data實現這裡的jQuery.1.x主要是指jQuery.1.11。 jQuery.acceptData() - 目標過濾因為jQuery.1.x是相容低版本瀏覽器的,所以需要處理大量的瀏覽器安全色性,在jQuery.1.x中設計的jQuery.data是基於給目標添加屬性來實現的,所以這其中找屬性找鑰匙找倉庫很是繁瑣,再加上IE低版本各種雷區,簡直喪心病狂了已經。找鑰匙找倉庫還好說,低版本IE的雷區一踩一個爆:所以jQuery單獨寫了一個jQuery.acceptData用於屏蔽雷區,特別針對下面的情況: applet和embed:這兩個標籤都是載入外部資源的,這兩個標籤在js裡可以操作的許可權簡直就是縮卵了——根本行不通,所以jQuery直接給幹掉了,直接讓他們不能放標籤。flash:早期的jQuery將所有的Object標籤納入雷區,後來發現IE下的flash還是可以自訂屬性的,於是針對IE的flash還是網開一面,放過了IE的flash,IE下載入flash的時候,需要對object指定一個叫做classId的屬性,它的值為:clsid:D27CDB6E-AE6D-11cf-96B8-444553540000。jQuery.acceptData配合jQuery.noData做的過濾:  internalData() - 掛載/讀取資料掛載和讀取資料方法是同一個(下面有分步分析):首先拿到鑰匙,也就是jQuery.expando擴充的屬性,然後根據鑰匙擷取倉庫,因為內部資料和使用者資料都是掛載在jQuery.cache下的,所以在jQuery.cache下開闢了jQuery.cache[鑰匙].data作為使用者資料存放的空間,而jQuery.cache[鑰匙]則存放jQuery的內部資料,將資料掛上之後,返回的結果是這個資料掛載的空間/位置,通過這個傳回值可以訪問到這個Element所有掛載的資料。 太長看起來噁心?來,我們一點點分析: 1、首先,檢測是否可以存放資料,可以的話初始化操作,針對變數id要注意,這裡的一直在找上面我們所說的掛載在Element上那個存放鑰匙的屬性,也就是jQuery.expando的值 + View Code2、如果沒有鑰匙(id),則為目標添加上鑰匙,代碼如下:  //沒有ID,則賦上IDif (!id) {    if (isNode) {       /*        DOM需要有一個全新的全域id        為DOM建立一個jQuery的全域id        低版本代碼:elem[ internalKey ] = id = ++jQuery.uuid;        這個deletedIds暫時忽略        id = elem[internalKey] = deletedIds.pop() || jQuery.guid++;        */    } else {        //而對象不需要        id = internalKey;    }}2、根據鑰匙,嘗試從cache中讀倉庫的位置,如果倉庫中還沒有這個目標的存放空間,則開闢一個,這裡特別針對了JSON做了處理:當調用JSON.stringify序列化對象的時候,會調用這個對象的toJSON方法,為了保證jQuery.data裡面資料的安全,所以直接重寫toJSON為一個空方法(jQuery.noop),這樣就不會曝露jQuery.data裡面的資料。另外一種說法是針對HTML5處理的dataAttr()(下面有講)使用JSON.parse轉換Object對象,而這個JSON可能是JSON2.js引入的:JSON2.js會為一系列原生類型添加toJSON方法,導致for in迴圈判定是否為空白對象的時候無法正確判定——所以jQuery搞了個jQuery.noop來處理這裡。  //從cache中沒有讀取到if (!cache[id]) {    //建立一個新的cache對象,這個toJson是個空方法    cache[id] = isNode ? {} : { toJSON: jQuery.noop };    /*        對於javascript對象,設定方法toJSON為空白函數,        以避免在執行JSON.stringify()時暴露快取資料。        如果一個對象定義了方法toJSON()        JSON.stringify()在序列化該對象時會調用這個方法來產生該對象的JSON元素    */}3、如果是Function/Object,則直接調用jQuery.extend掛資料,把$(Element).data({'name':'linkFly'})這種調用方式的資料掛到jQuery.cache /*  先把Object/Function的類型的資料掛上。調用方式 :  $(Element).data({'name':'linkFly'});  這裡的判定沒有調用jQuery.type()...當然在於作者的心態了...*/ if (typeof name === "object" || typeof name === "function") {    if (pvt) {//如果是內部資料        //掛到cache上        cache[id] = jQuery.extend(cache[id], name);    } else {        //如果是自訂資料,則掛到data上        cache[id].data = jQuery.extend(cache[id].data, name);    }}4、還有其他資料類型(String之類的)沒有掛載上,這裡把$(Element).data('name','value')的資料掛載上,最後要把這個data作為方法的傳回值,這個傳回值非常重要,從而實現也可以讀取資料的功能。 + View Code代碼上非常嚴謹,每一步都儘可能寫在最恰當的地方,這裡的方法會在jQuery最外層的API中和dataAttr()(下面會講)方法一起配合來實現掛載/讀取資料。internalRemoveData() - 移除資料資料移除方法移除資料方便比較簡單,但是當倉庫中沒有相應Element儲存的資料的時候,會直接從倉庫中刪除這個儲存空間(下面有分步分析):  1、和internalData()一樣,拿鑰匙。 //移除一個data到jQuery緩衝中if (!jQuery.acceptData(elem)) {    return;} var thisCache, i,    isNode = elem.nodeType,    cache = isNode ? jQuery.cache : elem,        //根據jQuery標識拿鑰匙    id = isNode ? elem[jQuery.expando] : jQuery.expando;//如果找不到緩衝,不再繼續if (!cache[id]) {    return;}2、找到倉庫儲存資料的位置,然後刪除資料,這裡充分的考慮了資料命名和Object參數的情況。  e3、如果資料全部刪除了,那麼倉庫儲存資料的空間也要被刪除,所以接下來針對這些情況進行了處理  //如果不是jQuery內部使用if (!pvt) {    delete cache[id].data;// 連data也刪除    //檢測還有沒有資料,還有資料則繼續    if (!isEmptyDataObject(cache[id])) {        return;    }}4、因為jQuery.data和jQuery.event事件系統直接掛鈎,所以這裡特別針對事件系統掛載的資料進行了刪除處理,jQuery.cleanData方法涉及jQuery.event,所以暫不解讀了。 //如果是Element,則破壞緩衝if (isNode) {    //和jQuery.event掛鈎,不分析了...    jQuery.cleanData([elem], true);} else if (support.deleteExpando || cache != cache.window) {    //不為window的情況下,或者可以瀏覽器檢測可以刪除window.屬性,再次嘗試刪除    delete cache[id];} else {    //否則,直接粗暴的null    cache[id] = null;}dataAttr()和jQuery.fn.data() - 針對HTML5的dataset和曝露APIdataAttr()是特別針對HTML5的dataset進行處理的方法,用處是讀取Element上HTML5的data-*屬性轉換到jQuery.data中,是針對HTML5的相容,典型的老夫就是要寵死你的方法:  jQuery.data實現很簡單......個屁啊,媽蛋啊看起來就是調用internalData()實現,實際上jQuery.fn.data更加的健壯,同時將各種內層的方法都聯結的惟妙惟肖,當然這也意味著效能更遜色一點, 1、針對擷取全部資料做處理,同時在內部標識上parsedAttrs,表示這個Element已經被轉換過HTML5屬性了:  if (key === undefined) {    //擷取    if (this.length) {        data = jQuery.data(elem);        //如果沒有標誌parsedAttrs的資料,則表示沒有進行過HTML5的屬性轉換        if (elem.nodeType === 1 && !jQuery._data(elem, "parsedAttrs")) {            i = attrs.length;            while (i--) {                //那麼轉換HTML5的屬性                if (attrs[i]) {                    name = attrs[i].name;                    if (name.indexOf("data-") === 0) {                        name = jQuery.camelCase(name.slice(5));                        //配合dataAttr進行轉換                        dataAttr(elem, name, data[name]);                    }                }            }            //放上屬性parsedAttrs,表示HTML5轉換完畢            jQuery._data(elem, "parsedAttrs", true);        }    }     return data;}2、如果不是讀取全部資料,則情況要麼是掛載資料,要麼是讀取資料,但在最後的一段代碼比較不錯,是internalData()和dataAttr()的配合使用針對HTML5 dataset的相容: + View Code這裡重點照顧最後一句,它實現了: 讀取資料:$(Element).data('demo');如果讀取不到,讀取HTML5的dataset資料並掛載到jQuery.cache中。如果到了這裡,那麼調用方式會是:$(Elment).data('name'),這時候的處理方法就是: jQuery.data底層是internalData(),當第三個參數為空白的時候,則是讀取資料internalData()如果讀取不到資料,則調用dataAttr(),而dataAttr第三個參數為undefined的時候,則會讀取HTML5的dataset,然後再調用jQuery.data()(注意不是jQuery.fn.data)再掛一次資料。好的各位爺,至此jQuery.1.x代碼已經讀完了,要不您老喝點茶看看窗外放鬆一下消化一下上面的代碼?:jQuery.expando是鑰匙的關鍵,將jQuery.expando的值掛在Element上,就好像在你身上掛了一張銀行卡,而銀行卡的密碼,則是jQuery.guid(累加不重複)。通過鑰匙找到倉庫,進行操作。internalData()的思路很值得借鑒,在掛資料的時候同時取資料,尤其在jQuery.cache這個相對比較複雜的環境裡,如何更高效的取資料本身就是一件值得思考的事情。internalRemoveData()實現了深度刪除資料,儘可能讓資料彷彿從未存在過,並且嘗試了多種刪除。dataAttr()是針對HTML5特別的相容處理。internalData()方法非常的嚴謹,但是它仍然只是為了掛載資料和移除資料而生,非常純粹而簡單的工作著,真正讓jQuery健壯的是jQuery.fn.data。jQuery.2.x中jQuery.data實現這裡的jQuery.2.x主要是指jQuery.2.1。 在jQuery.2.x中,jQuery.data終於決定被好好深造一下了,過去1.x的代碼說多了都是淚,jQuery.2.x沒有了相容性的後顧之憂,改寫後的代碼讀起來簡直不要太舒適啊。 在jQuery.2.x中,為資料緩衝建立了Data對象,一個Data對象表示一個資料倉儲——使用者資料和內部資料各自使用不同的Data對象,這樣就不需要在倉庫裡翻來翻去的尋找資料存放區的位置了(jQuery.cache[鑰匙]和jQuery.cache[鑰匙].data),思路上,仍然和jQuery.1.x一致,採用擴充屬性的方式實現,關鍵點在Data.prorotype.key()上。 Data對象 - 資料倉儲Data對象經過封裝以後衍生了這些API: key:專門用來擷取和放置Element的鑰匙。set/get:放置和擷取資料access:通用API,根據參數既然可以放置也可以擷取資料remove:移除資料hasData:檢測是否有資料discard:丟棄掉這個Element的儲存空間  其他的實現都比較簡單,我們需要關注鑰匙這裡,也就是Data.prototype.key()。 Data.prototype.key() - 鑰匙+ View Code因為使用者資料和jQuery內部資料通過Data分離,所以set/get在拿到鑰匙之後都比較簡單。 access() - 通用介面在建立Data對象的時候,順便為jQuery建立了靜態方法——jQuery.access:通用的底層方法,既能設定也能讀取,它應用在jQuery很多API中,例如:Text()、HTML()等。 + View CodejQuery.fn.data() - 曝露API相比jQuery.1.x代碼更加的細膩了許多,這裡配合著上面定義的access()使用,為每一個迴圈的jQuery項設定和讀取資料,閱讀起來比較輕鬆。 + View Code各位看官到了這裡可以繼續小憩一下,後面我們再來談談關於這個jQuery.data更多有意思的事情...總結一下,jQuery.2.x的緩衝設計理念清晰,最主要的就是封裝成了Data對象以後將使用者資料和jQuery內部使用的資料隔離開,這是最大的改進。移動端的Zepto裡的Zepto.data是jQuery.data.2.x的濃縮版。其他實現這些實現都是在司徒正美的《javascript架構設計》 - "資料緩衝系統"一章裡讀到的,有必要宣傳和感謝一下這本書,瞭解了很多代碼的由來促進了理解。 這些實現其實都是針對鑰匙怎麼交給Element這個問題上進行的探索。 valueOf()重寫在jQuery.2.x最初設計的jQuery.data中,作者也在為Element掛載這個expando屬性作為鑰匙而頭疼,於是給出了另外一種鑰匙的掛載方法——重寫valueOf()。 Waldron 在為Element掛載鑰匙的時候,不再給這個Element聲明屬性,而是通過重寫Element的valueOf方法實現。 雖然我翻了jQuery.2.0.0 - jQuery.2.1.1都沒有找到這種做法,但覺得還是有必要提一下:function Data() {        this.cache = {};    };    Data.uid = 1;    Data.prototype = {        locker: function (owner) {            var ovalueOf,                unlock = owner.valueOf(Data);            /*            owner為元素節點、文檔對象、window            傳遞Data類,如果返回object說明沒有被重寫,返回string則表示已被重寫            整個過程被jQuery稱之為開鎖,通過valueOf得到鑰匙,進入倉庫            */            if (typeof unlock !== 'string') {                //通過閉包儲存,也意味著記憶體消耗更大                unlock = jQuery.expando + Data.uid++;                //緩衝原valueOf方法                ovalueOf = owner.valueOf;                Object.defineProperty(owner, 'valueOf', {                    value: function (pick) {                        //傳入Data                        if (pick === Data)                            return unlock; //返回鑰匙                        return ovalueOf.apply(owner); //返回原valueOf方法                    }                });            }            if (!this.cache[unlock])                this.cache[unlock] = {};            return unlock;        },        get: function (owner, key) {            var cache = this.cache[this.locker(owner)];            return key === undefined ? cache : cache[key];        },        set: function (owner, key, value) {            //略        }        /*其他方法略*/    };思路上很是新穎——因為在js中幾乎所有的js資料類型(null,undefined除外)都擁有valueOf/toString方法,所以直接重寫Element的valueOf,在傳入Data對象的時候,返回鑰匙,否則返回原valueOf方法——優點是鑰匙隱性掛到了Element上,保證了Element的乾淨和無需再考慮掛屬性兼不相容等問題了,而缺點就是採用閉包,所以記憶體消耗更大,或許jQuery也覺得這種做法的記憶體消耗不能忍,所以仍未採用——相比較放置鑰匙到Element的方式,還是後者更加的純粹和穩定。 Array.prototype.indexOf()Array.prototype.indexOf()是ECMAScript 5(低版本瀏覽器可以使用代碼類比)定義的方法——可以從一組Array中檢索某項是否存在?存在返回該項索引:不存在則返回-1。聽起來很相似?沒錯,它就是String.prototype.indexOf()的數組版。 正是因為提供了針對數組項的尋找,所以可以採用新的思路: 1、將使用data()方法掛載資料的Element通過閉包緩衝到一個數組中2、當下次需要檢索和這個Element關聯的資料的時候,只需要通過Array.ptototype.indexOf在閉包中尋找到這個數組即可,而閉包中這個數組尋找到的索引,就是鑰匙。代碼如下: (function () {    var caches = [],        add = function (owner) {            /*             //拆開來是這樣子的            var length = caches.push(owner);//返回Array的length            return caches[length - 1] = {};//建立對象並返回            */            return caches(caches.push(owner) - 1) = {};        },    addData = function (owner, name, data) {        var index = caches.indexOf(owner), //尋找索引,索引即是鑰匙        //擷取倉庫            cache = index === -1 ? add(owner) : caches[index];        //針對倉庫放資料即可    }    //其他代碼略})();這樣就不需要在Element上掛載自訂的屬性(鑰匙)了——然而因為每個使用過data()的Element都會在緩衝下來,那麼記憶體的消耗必不可免,相比上一種重寫valueOf重寫消耗更加的不能直視,這是一個有趣但並不推薦的解決方案。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.