jQuery源代碼解析(1)—— jq基礎、data緩存系統
閑話
jquery 的源代碼已經到了1.12.0
版本號。據官網說1版本號和2版本號若無意外將不再更新,3版本號將做一個架構上大的調整。但預計能兼容IE6-8的。或許這已經是最後的樣子了。
我學習jq的時間非常短,應該在1月。那時的版本號還是1.11.3,通過看妙味課堂的公開課視頻和文檔裏的全部api的註解學習。
源代碼則是近期些日子直接生啃。跳過了sizzle和文檔處理的部分(待業狗壓力大。工作以後再看)。關註data
、ready
、event
、queue
、Defferred
(jq的promise編程)、ajax
、animation
的處理,初看甚至有點惡心。耐著性子堅持卻嘆其精妙。在這裏記錄下來加深印象。
(本文採用 1.12.0 版本號進行解說,用 #number
來標註行號)
jQuery初始化
總體封裝上,考慮了兩點(寫輪子的重要參考)
模塊化支持:通過 noGlobal
變量來決定是否綁定到全局變量,返回值為jQuery
沖突避免:保存window.$
window.jQuery
,能夠調用noConflict
還原,返回jQuery對象。(noGlobal為true時不須要)
主體使用了例如以下結構。抽離 if
分支。僅僅關註主體邏輯的書寫
(function( a, fun ) {
// 推斷是否調用、怎樣調用fun
})(a, function( _a, _b ) {
// 詳細邏輯
});
/* ---- 差別於例如以下模式 ----*/
(function( _a, _b) {
if (推斷A) {
// 調整參數或退出
} else if (推斷B) {
// 調整參數或退出
} ...
// 這裏開始時詳細邏輯
})( a );
[源代碼]
(function( global, factory ) {
if ( typeof module === "object" && typeof module.exports === "object" ) {
module.exports = global.document ?
factory( global, true ) :
// 若無window,則模塊存為函數,可取出通過傳入window來調用一次
// noGlobal為false,一定會汙染全局
function( w ) {
if ( !w.document ) {
throw new Error( "jQuery requires a window with a document" );
}
return factory( w );
};
} else {
factory( global );
}
})(typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
/* ---- jQuery詳細邏輯 ----*/
...
// 對AMD的支持,#10991
if ( typeof define === "function" && define.amd ) {
define( "jquery", [], function() {
return jQuery;
} );
}
// 保存之前的 window.$ window.jQuery
var _jQuery = window.jQuery,
_$ = window.$;
jQuery.noConflict = function() {
if ( window.$ === jQuery ) {
window.$ = _$;
}
if ( deep && window.jQuery === jQuery ) {
window.jQuery = _jQuery;
}
return jQuery;
};
// 模塊化時,不設置全局
if ( !noGlobal ) {
window.jQuery = window.$ = jQuery;
}
return jQuery;
});
jQuery工廠結構
jq為了能有$.func、 $().func兩種調用方法,選擇了共享 jQuery.prototype
,return new jQuery.fn.init
這並非唯一的方式(能夠例如以下),之所以選擇如此,個人覺得應該是使用頻率太高,這樣每次能夠省掉兩次類型推斷。
而 jQuery.fn
我想也是起到簡寫、別名的作用
jQuery.extend/fn.extend
則決定了jq重要的插件擴展機制
var jQuery = function( selector, context ) {
if ( this instanceof jQuery) {
return new jQuery( selector, context );
}
// 詳細邏輯
}
[源代碼]
// #71
var jQuery = function( selector, context ) {
return new jQuery.fn.init( selector, context );
};
// #91, 原型的別名 fn,省字符
jQuery.fn = jQuery.prototype = {
...
};
// #175, jQuery 擴展方法extend定義,僅僅填一個參數表示對this進行擴展
jQuery.extend = jQuery.fn.extend = function(){
...
};
// #2866, 支持選擇器,節點html,element,jq對象,函數
var init = jQuery.init = function( selector, context, root ) {
...
};
// #2982, 跟jQuery共享原型對象
init.prototype = jQuery.fn;
jQuery鏈式調用
精髓:通過 return this
, return this.pushStack()
, return this.prevObject
實現鏈式調用、增棧、回溯
[源代碼]
// # 122, 創建一層新的堆棧, 並引用 prevObject
pushStack: function( elems ) {
// merge -> #433, 支持把數組、類數組的0-length項加入到第一個參數
var ret = jQuery.merge( this.constructor(), elems );
ret.prevObject = this;
ret.context = this.context;
return ret;
};
// #164, 能夠回溯上一級 preObject
end: function() {
return this.preObject || this.constructor();
}
// #3063, 加入到新層 this.constructor()
add: function( selector, context ) {
return this.pushStack(
// 去重排序
jQuery.uniqueSort(
// 合並到 this.get()
jQuery.merge( this.get(), jQuery( selector, context ) )
)
);
},
addBack: function( selector ) {
// add將把結果pushStack
return this.add( selector == null ?
this.preObject :
// 可做一次過濾
this.prevObject.filter( selector )
);
}
看到這,說明你真的是個有耐心的boy了。我們上主菜!
IE內存泄露
講data緩存前,首先要必須介紹一下IE6-8的內存泄露問題。
IE低版本號dom採用COM組件的形式編寫,垃圾清理機制。使用的引用計數的方式,無法正確的處理包括dom元素的環狀的循環引用。
即使刷新頁面內存也不會被釋放。
一般是怎樣產生循環引用的呢?
此例中。當前作用域裏包括變量a (引用了dom元素),通過綁定事件onclick,使dom引用了事件函數。
可是函數在聲明時,會把當前所在活動對象的作用域鏈保存在自身的scope屬性。從而引用了當前環境定義的全部變量。而變量a指向dom。從而產生循環引用。須要手動把變量對dom的引用解除
{ // 某個scope作用域
var a = document.getElementById(‘id_a‘);
// dom -> fun
a.onclick = function(e) {
};
// fun -> a -> dom , 解除對dom元素的引用, fun -> a -X- dom
a = null;
}
jQuery.data原理
jq內部實現了一個緩存機制,用於分離dom對函數等對象的引用(函數對dom的引用取決於函數本身的特性、定義的位置,這個沒法子)。怎樣能做到呢?與 策略模式 的思路一致——字符串約定。
jQuery.extend({
// #247, 構造一個基本不可能被占用的屬性, 除去"."
expando: "jQuery" + ( version + Math.random() ).replace(/\D/g, ""),
// #507, 全局使用的 guid 號碼
guid: 1,
// #4014, 緩存空間
cache: {}
});
/* ---- 原理示意 ---- */
var a = document.getElementById("id_a");
// 每一個元素的 jQuery.expando 分到唯一的 guid 號碼
a[jQuery.expando] = a[jQuery.expando] || jQuery.guid++;
// jQuery.cache 上元素的 guid 相應的位置為該對象的緩存空間
jQuery.cache[a[jQuery.expando]] = {};
通過 jQuery.data
的封裝能夠輕松實現數據的存取,可是這樣就結束了麽?太天真了!
這樣做也會產生衍生的問題。盡管不產生循環引用,但對象綁定在jQuery.cache上。即使dom對象被移除。數據仍然不會因此消失。並且假設是函數,仍然會通過引用環境的變量單向引用著dom,導致dom元素也不消失。盡管刷新後內存會清出,不如循環引用嚴重,但也是問題了。
看起來仍然須要手動設置變量為null,仿佛回到原點,可是數據還在,比之前更不如。解決方法事實上非常easy,就是移除節點時調用 jQuery.cleanData
,data沒有被不論什麽對象引用,自然的回收。
可是問題仍然沒解決,由於比如綁定事件。即使函數放在jQuery.cache中,也至少有一個觸發函數綁定在dom上。因此 jQuery.event.add( elem, types, handler, data, selector )
中的elem返回前被設為null ,見源代碼 #4961。所以如非事事通明。盡量使用jq提供的方式刪除節點、綁定事件等
function remove( elem, selector, keepData ) { // #6107, #6255為正式方法
var node,
elems = selector ? jQuery.filter( selector, elem ) : elem,
i = 0;
for ( ; ( node = elems[ i ] ) != null; i++ ) {
if ( !keepData && node.nodeType === 1 ) {
// 清除數據
jQuery.cleanData( getAll( node ) );
}
if ( node.parentNode ) {
if ( keepData && jQuery.contains( node.ownerDocument, node ) ) {
setGlobalEval( getAll( node, "script" ) );
}
// 刪除節點
node.parentNode.removeChild( node );
}
}
return elem;
}
jQuery架構方法
jQuery在此之上考慮了幾點:
核心
對普通對象和dom對象做了區分。由於普通對象的垃圾回收機制(GC)使用的不是引用計數,不會內存泄露。
其次data機制本身添加了復雜度不說,對象解除引用後綁定的數據還須要手動銷毀。反而造成內存的無端占用。
普通對象綁定在自身的
jQuery.expando
屬性上。初始化為 { toJSON: jQuery.noop },防止序列化時暴露數據。dom對象綁定在 jQuery.cache[ ele[jQuery.expando] ] ,初始化為 {}
私用和公用隔離,如 jQuery.cache[ guid ]、jQuery.cache[ guid ].data。內部的事件、隊列等等都使用的私有空間調用 _data 方法,而用戶儲存數據調用的 data 方法則是私有,由參數
pvt
決定,true代表私有當移除數據後。jQuery.cache[ guid ]或 jQuery.cache[ guid ].data對象不再有數據。則移除該對象釋放內存。
且當移除緩存對象時,綁定在elem的事件函數也將被移除
特性
支持通過簡單的自己定義配置來添加不支持緩存的特例類型
擴展支持讀取 elem.attributes 中
data-
前綴定義的數據以小駝峰書寫為標準讀取方式,內部進行相應轉換
jQuery.data結構
使用常見的外觀模式。定義核心功能。通過再封裝實現個性化的使用規則,以供其它模塊或外部調用
核心功能:(特點:參數多而全,邏輯負責全面)
jQuery.internalData( elem, name, data, pvt ) 為dom和對象設置data數據。支持 -> 核心12 jQuery.internalRemoveData( elem, name, pvt ) 移除dom或對象上指定屬性的data數據-> 核心3 jQuery.cleanData( elems, forceAcceptData ) 由於刪除data時還要刪除elem本身上綁定的觸發事件函數,因此不能簡單 delete cache[id]。 單獨封裝,被事件系統和 jQuery.internalRemoveData使用 ->核心3
外觀:(工具方法、實例方法)
jQuery.hasData( elem ) 直接在相應的 `cache` 中查找。 jQuery._data( elem, name, data ) jQuery.\_removeData( elem, name )`用於內部私有調用。封裝了核心方法,簡化了傳參數量。 jQuery.data( elem, name, data ) jQuery.removeData( elem, name ) 用於外部公共調用。封裝了核心方法,簡化了傳參數量。
jQuery.fn.data( key, value ) jQuery.fn.removeData( key, value ) 封裝了 jQuery.data jQuery.removeData ,遍歷this來分別調用
鉤子:埋在某個環節直接運行,依據須要實現終止流程、改變特性的功能,或不產生不論什麽影響
acceptData( elem ) #3762,特性1。過濾 dataAttr( elem, key, data ) #3780。特性2,data的值不存在則會訪問元素的attribute。返回data,且值會緩存到cache jQuery.camelCase( string ) #356,特性3。小駝峰
[核心 + 外觀 + 鉤子] 感覺應該算一種常見好用的架構方式了。
jq中用到了非常多 jQuery.some()
-> 核心, jQuery.fn.some()
-> 外觀 的形式,也都幾乎相同。
這裏提一下我所理解的鉤子,Callback的四個字符串特性就相似於四個鉤子。在最常見的幾個需求分歧上埋設,發展了觀察者模式的4種特性支持,event的眾多鉤子 標準方式轉換 + 個例bug修正
完畢了兼容的大業與主邏輯的統一。
另外不得不提一個在此維度之外的優化方案 —— 提煉反復代碼。
核心邏輯是不可省的。通常支持數種方式定義參數,把其變化為某種固定形式的參數傳入來運行遞歸是分離函數內部個性化與核心邏輯的第一步,還能夠進一步,抽出此部分邏輯。保持主邏輯的純粹,變成外觀部分內的邏輯進行 功能增強。jq再進了一步,由於大量方法都支持參數推斷存取、對象或字符串均可、map映射,分離出 access( elems, fn, key, value, chainable, emptyGet, raw )
#4376
,核心 jQuery.xxx(),外觀jQuery.fn.xxx() 裏調用 access`。
// access( elems, fn, key, value, chainable, emptyGet, raw )
// 直觀語義是讓forEach( elems, fn( elem, key, value ) ), 支持類型推斷實現映射等功能
css: function( name, value ) { // #7340, 任意舉的樣例
// 第二個參數要麽是 jQuery.someThing
// 或者是封裝了 jQuery.someThing.apply( ... ) 的函數。內含
return access( this, function( elem, name, value ) {
var styles, len,
map = {},
i = 0;
// 新增一樣僅僅針對css的個性化參數處理
if ( jQuery.isArray( name ) ) {
styles = getStyles( elem );
len = name.length;
for ( ; i < len; i++ ) {
map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
}
return map;
}
return value !== undefined ?
jQuery.style( elem, name, value ) :
jQuery.css( elem, name );
}, name, value, arguments.length > 1 );
}
可是,data緩存系統並沒有這樣使用。
原因也非常easy,基礎的共性部分支持不同不支持映射,如上面的css是共性同樣的情況下能夠添加個性,但不同的情況就要又一次抽出、或寫在外觀裏、或寫在核心代碼裏使用遞歸。
[源代碼]
/* ---------------------------------- 1. 相關代碼(掃一下) ---------------------------------- */
jQuery.extend({
// #247, 構造一個基本不可能被占用的屬性, 除去"."
expando: "jQuery" + ( version + Math.random() ).replace(/\D/g, ""),
// #507, 全局使用的 guid 號碼
guid: 1,
// #4014, 緩存空間
cache: {},
noData: {
// 為 true 的不能夠
"applet ": true,
"embed ": true,
// ...but Flash objects (which have this classid) *can* handle expandos
"object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
},
});
// #3748, support.deleteExpando
( function() {
var div = document.createElement( "div" );
// Support: IE<9
support.deleteExpando = true;
try {
delete div.test;
} catch ( e ) {
support.deleteExpando = false;
}
// Null elements to avoid leaks in IE.
div = null;
} )();
// #284, 可用於檢測 elem 的公有 cache.data 是否已空
jQuery.isEmptyObject = function( obj ) {
var name;
for ( name in obj ) {
return false;
}
return true;
};
// #3814, 檢測 elem 的私有 cache 緩存是否已空
function isEmptyDataObject( obj ) {
var name;
for ( name in obj ) {
// if the public data object is empty, the private is still empty
if ( name === "data" && jQuery.isEmptyObject( obj[ name ] ) ) {
continue;
}
if ( name !== "toJSON" ) {
return false;
}
}
return true;
}
/* ---------------------------------- 2. 鉤子(瞅瞅) ---------------------------------- */
// #3762
var acceptData = function( elem ) {
// 比對禁止列表 jQuery.noData, 為 true 必定不能
var noData = jQuery.noData[ ( elem.nodeName + " " ).toLowerCase() ],
// 普通對象都會按 nodeType = 1 處理,通過篩選
nodeType = +elem.nodeType || 1;
return nodeType !== 1 && nodeType !== 9 ?
false :
// noData不存在(黑名單) 或 存在且classid與noDta同樣但不為true(白名單)
!noData || noData !== true && elem.getAttribute( "classid" ) === noData;
};
function dataAttr( elem, key, data ) {
// 正確姿勢: dataAttr( elem, key, jQuery.data( elem )[ key ] );
// data 不存在。則查找 HTML5 data-* attribute,並存入 jQuery.data( elem, key, data )
// 返回 data
if ( data === undefined && elem.nodeType === 1 ) {
var name = "data-" + key.replace( /([A-Z])/g, "-$1" ).toLowerCase();
data = elem.getAttribute( name );
if ( typeof data === "string" ) {
try {
// "true" -> true , "false" -> false , "23" -> 23
// "{ \"a\": 1}" -> {"a": 1}, "[1, ‘2‘]" -> [1, ‘2‘]
// 或 data 本身
data = data === "true" ?
true :
data === "false" ?
false :
data === "null" ? null :
+data + "" === data ? +data :
/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/.test( data ) ?
jQuery.parseJSON( data ) :
data;
} catch ( e ) {}
// 保存
jQuery.data( elem, key, data );
} else {
data = undefined;
}
}
return data;
}
// #356, 正則變量替換了,本身在 #80
jQuery.camelCase = function( string ) {
// -ms-abc -> msAbc , a-abc -> aAbc
return string.replace( /^-ms-/, "ms-" ).replace( /-([\da-z])/gi, fcamelCase );
};
/* ---------------------------------- 3. 核心(關鍵) ---------------------------------- */
// #3830, 加入緩存
function internalData( elem, name, data, pvt /* true 為私,存cache;false 為公。存cache.data */ ) {
// 鉤子。黑白名單
if ( !acceptData( elem ) ) {
return;
}
var ret, thisCache,
internalKey = jQuery.expando,
// dom 和 object 區分對待
isNode = elem.nodeType,
// object 不緩存,存自身 jQuery.expando 屬性上
cache = isNode ?
jQuery.cache : elem,
// 沒設置過說明 第一次
id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;
// 1. 第一次僅僅能寫,不能讀。否則 return
if ( ( !id || !cache[ id ] || ( !pvt && !cache[ id ].data ) ) &&
data === undefined && typeof name === "string" ) {
return;
}
// 2. 第一次寫。先初始化 [ 屬性、緩存對象 cache ]
// dom -> jQuery.cache[ elem[ internalKey ] ]。object -> object[internalKey]
if ( !id ) {
// Only DOM nodes need a new unique ID for each element since their data
// ends up in the global cache
if ( isNode ) {
id = elem[ internalKey ] = deletedIds.pop() || jQuery.guid++;
} else {
id = internalKey;
}
}
if ( !cache[ id ] ) {
cache[ id ] = isNode ? {} : { toJSON: jQuery.noop };
}
// 3. 寫入與讀取
// write特例:支持 name 參數為 { "key" : value } 或 函數。存入數據
if ( typeof name === "object" || typeof name === "function" ) {
// 公私不同
if ( pvt ) {
cache[ id ] = jQuery.extend( cache[ id ], name );
} else {
cache[ id ].data = jQuery.extend( cache[ id ].data, name );
}
}
thisCache = cache[ id ];
// thisCache 依據pvt索引到了應該被賦值的對象
if ( !pvt ) {
if ( !thisCache.data ) {
thisCache.data = {};
}
thisCache = thisCache.data;
}
// 寫入,小駝峰為標準
if ( data !== undefined ) {
thisCache[ jQuery.camelCase( name ) ] = data;
}
// Check for both converted-to-camel and non-converted data property names
// If a data property was specified
if ( typeof name === "string" ) {
// 這不是重點。也能夠讀非駝峰。正常使用並不會有
ret = thisCache[ name ];
if ( ret == null ) {
// 讀這裏。不管讀寫,都要讀一次
ret = thisCache[ jQuery.camelCase( name ) ];
}
} else {
// 無 name , 直接讀 cache
ret = thisCache;
}
return ret;
}
// #3922, 刪除 公/私 緩存屬性 或 緩存。
// 刪完屬性若變空cache,將移去。會處理掉 elem 上 綁定的 event
function internalRemoveData( elem, name, pvt ) {
// 鉤子,黑名單者,直接 return
if ( !acceptData( elem ) ) {
return;
}
var thisCache, i,
isNode = elem.nodeType,
cache = isNode ?
jQuery.cache : elem,
id = isNode ?
elem[ jQuery.expando ] : jQuery.expando;
// 緩存空間未初始化,return
if ( !cache[ id ] ) {
return;
}
// 1. name 存在,刪除 name 屬性值
if ( name ) {
thisCache = pvt ? cache[ id ] : cache[ id ].data;
if ( thisCache ) {
// 1.1 支持數組定義多屬性,此處把字符串形式也轉為數組[name]
// next step: 統一叠代刪除
if ( !jQuery.isArray( name ) ) {
// 這不是重點。也能夠讀非駝峰。
正常使用並不會有
if ( name in thisCache ) {
name = [ name ];
} else {
// 看這裏,轉換為小駝峰讀
name = jQuery.camelCase( name );
if ( name in thisCache ) {
name = [ name ];
} else {
// 能夠字符串空格隔開多個,均變成小駝峰
name = name.split( " " );
}
}
} else {
// If "name" is an array of keys...
// When data is initially created, via ("key", "val") signature,
// keys will be converted to camelCase.
// Since there is no way to tell _how_ a key was added, remove
// both plain key and camelCase key. #12786
// This will only penalize the array argument path.
name = name.concat( jQuery.map( name, jQuery.camelCase ) );
}
// 1.2 刪
i = name.length;
while ( i-- ) {
delete thisCache[ name[ i ] ];
}
// 1.3 假設 cache 刪除後沒空。結束 return
// 假設空了, 與 name 不存在的情況一樣直接刪除 data
if ( pvt ? !isEmptyDataObject( thisCache ) : !jQuery.isEmptyObject( thisCache ) ) {
return;
}
}
}
// 2. 依據 pvt 推斷,false 刪除公有
if ( !pvt ) {
delete cache[ id ].data;
// cache 還沒空,能夠閃了,return
// cache 空了,合並到 pvt 為true,私有cache 刪除
if ( !isEmptyDataObject( cache[ id ] ) ) {
return;
}
}
// 3. pvt 為 true, 刪除私有 cache
// 3.1 為節點時。若cache[events]裏還有事件,把 elem 綁定的事件函數刪除
if ( isNode ) {
jQuery.cleanData( [ elem ], true );
// 3.2 普通對象時
} else if ( support.deleteExpando || cache != cache.window ) {
// 能刪則 delete
delete cache[ id ];
// 不能刪, undefined
} else {
cache[ id ] = undefined;
}
}
// #6192, 函數內包括 刪除事件隊列時刪除elem相應type的綁定事件 的功能
// cleanData 不僅被 internalRemoveData 內部調用,remove節點的時候也$().remove調用,因此支持了elems 數組
jquery.cleanData = function( elems, /* internal */ forceAcceptData ) {
var elem, type, id, data,
i = 0,
internalKey = jQuery.expando,
cache = jQuery.cache,
attributes = support.attributes,
special = jQuery.event.special;
// 支持 elems 數組叠代
for ( ; ( elem = elems[ i ] ) != null; i++ ) {
// 鉤子
if ( forceAcceptData || acceptData( elem ) ) {
id = elem[ internalKey ];
data = id && cache[ id ];
if ( data ) {
// 1. 存在事件隊列
if ( data.events ) {
// 叠代,刪除綁在 elem 上的觸發函數
for ( type in data.events ) {
// 盡管綁定在data上的事件。都轉換成標準的 eventType
// 但標準的 eventtype 可能不被兼容
// special.setup 鉤子在綁定觸發函數時會hack一次
// 須要該方法找到綁定在elem上事件觸發函數的真正類型並刪除
if ( special[ type ] ) {
jQuery.event.remove( elem, type );
// 普通情況,直接刪除
} else {
jQuery.removeEvent( elem, type, data.handle );
}
}
}
// 2. 不存在(或已刪去)事件隊列
if ( cache[ id ] ) {
delete cache[ id ];
// Support: IE<9
// IE does not allow us to delete expando properties from nodes
// IE creates expando attributes along with the property
// IE does not have a removeAttribute function on Document nodes
if ( !attributes && typeof elem.removeAttribute !== "undefined" ) {
elem.removeAttribute( internalKey );
} else {
elem[ internalKey ] = undefined;
}
deletedIds.push( id );
}
}
}
}
}
/* ---------------------------------- 4. 外觀(接口API) ---------------------------------- */
// #4013
jQuery.extend( {
// cache、noData 上面已經提前寫了
cache: {},
noData: {
"applet ": true,
"embed ": true,
"object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
},
// 直接從緩存 cache 查找推斷。dom 與 object差別對待
hasData: function( elem ) {
elem = elem.nodeType ?
jQuery.cache[ elem[ jQuery.expando ] ] : elem[ jQuery.expando ];
return !!elem && !isEmptyDataObject( elem );
},
// 公用。pvt 無
data: function( elem, name, data ) {
return internalData( elem, name, data );
},
removeData: function( elem, name ) {
return internalRemoveData( elem, name );
},
// 私用,pvt true
_data: function( elem, name, data ) {
return internalData( elem, name, data, true );
},
_removeData: function( elem, name ) {
return internalRemoveData( elem, name, true );
}
} );
// #4049,實例方法
jQuery.fn.extend( {
data: function( key, value ) {
var i, name, data,
elem = this[ 0 ],
attrs = elem && elem.attributes;
// Special expections of .data basically thwart jQuery.access,
// so implement the relevant behavior ourselves
// 1. key不存在。
獲得 data緩存 全部值
if ( key === undefined ) {
if ( this.length ) {
data = jQuery.data( elem );
if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) {
i = attrs.length;
while ( i-- ) {
// Support: IE11+
// The attrs elements can be null (#14894)
if ( attrs[ i ] ) {
name = attrs[ i ].name;
if ( name.indexOf( "data-" ) === 0 ) {
name = jQuery.camelCase( name.slice( 5 ) );
// 鉤子,data[name]若無。則搜索data- attribute,並賦值給 data[ name ]
dataAttr( elem, name, data[ name ] );
}
}
}
jQuery._data( elem, "parsedAttrs", true );
}
}
return data;
}
// 2. key 是 "object"
// internalData 已經支持了 "object" 參數。因此直接叠代
if ( typeof key === "object" ) {
return this.each( function() {
jQuery.data( this, key );
} );
}
return arguments.length > 1 ?
// 3. 叠代寫入 key -> value
this.each( function() {
jQuery.data( this, key, value );
} ) :
// 4. 讀取 key 屬性值, 沒有則嘗試讀data- attribute,並賦值給 data[ name ]
elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : undefined;
},
removeData: function( key ) {
return this.each( function() {
jQuery.removeData( this, key );
} );
}
} );
最後再說幾點:
$(elem).data("key", "name")
與$.data($(elem), "key", "name")
的差別是前者內部元素被叠代。綁定在元素(dom)上,而後者綁定在$(elem)
對象(object)上,差別不言自明。對於支持對象等多種參數形式的邏輯本身很多其它放在外觀裏。這裏在
internalData
。由於公有私有不止一個外觀,避免反復要麽抽出相似access
使用。要麽放到公共方法中。而之所以不放在終於的實例data
方法中,由於工具方法已經在jq模塊內部被多次使用了,這樣能夠有效簡化內部操作。dataAtrr
之所以在實例data
方法中才出現被使用。是由於僅僅實用戶調用的時候dom才載入完呀,才會產生這個須要。
jQuery源代碼解析(1)—— jq基礎、data緩存系統