作者:Jiang, Jinlin
在我们使用jQuery中,会用到$(element).data()方法存取赋值到元素上的数据。通过该方法,我们每次通过selector获取该元素时,总能获取其赋值的数据。今天,我们就来看看jQuery是如何实现的。
首先,我们先从data方法看起:
data方法接受两种参数形式,第一种通过key,value形式将数据赋值于元素之上。第二种通过key形式获取赋值的数据。(其中,如果key为空则导出所有key-value map)
通过data方法不但可以获取通过data本身传入的数据,也可以直接获取元素上直接[data-*]形式赋值的数据。接下来,就让我们看一看jQuery的源代码实现(我会在代码中进行标注,当然如果你不愿意看细节,之后会有一个图示来标示jQuery的数据赋值):
jQuery.fn.extend({ data: function( key, value ) { var i, name, data, elem = this[0], // 如果存在元素则获取它的attributes属性 attrs = elem && elem.attributes;
// Special expections of .data basically thwart jQuery.access, // so implement the relevant behavior ourselves
// Gets all values // 如果没有设定key,则将元素包含数据全部导出 if ( key === undefined ) { if ( this.length ) { // 获取元素数据,之后会进入分析 data = jQuery.data( elem );
// nodeType属性用于获取类型,其中1为DOM元素类型。因而其只会返回dom元素赋值的数据 // _data为内部使用方法,之后将会详细介绍 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; // 检查是否存在以data-开头的属性 if ( name.indexOf( "data-" ) === 0 ) { // 使用驼峰法表示属性名,例如data-user-mail,将会被转成userMail name = jQuery.camelCase( name.slice(5) ); // 获取[data-*]值并赋值数据 dataAttr( elem, name, data[ name ] ); } } } // 标示该元素已经获取了[data-*]值,之后添加的data-*属性将不再获取 // 你可以进行如下代码尝试一下结果: // $(ele).attr("data-test", 123); // $(ele).data(); => {test: 123} // $(ele).attr("data-test", 321); // $(ele).data(); => {test: 123} jQuery._data( elem, "parsedAttrs", true ); } }
return data; }
// 如果传入的是一个object对象,则将其包含的所有key-value传入 // Sets multiple values if ( typeof key === "object" ) { return this.each(function() { jQuery.data( this, key ); }); }
return arguments.length > 1 ? // 对所有元素赋值数据(应该尽量避免选择器同时选择多个进行数据赋值) // Sets one value this.each(function() { jQuery.data( this, key, value ); }) : // 返回赋值数值 // Gets one value // Try to fetch any internally stored data first elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : undefined; }, }); |
jQuery的data与_data方法调用的都是调用内部的internalData方法,只是参数不同:
jQuery.extend({ cache: {}, data: function( elem, name, data ) { return internalData( elem, name, data ); }, // For internal use only. _data: function( elem, name, data ) { return internalData( elem, name, data, true); }, }); |
而dataAttr方法:
function dataAttr( elem, key, data ) { // 如果内部没有发现数据,则尝试获取html5的[data-*]属性的数值 // If nothing was found internally, try to fetch any // data from the HTML5 data-* attribute if ( data === undefined && elem.nodeType === 1 ) { // 将驼峰法转换成data-*-*形式 var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
data = elem.getAttribute( name );
if ( typeof data === "string" ) { try { // 对string值进行类型转换,所以如果值符合转换条件就会被转掉。 data = data === "true" ? true : data === "false" ? false : data === "null" ? null : // Only convert to a number if it doesn't change the string +data + "" === data ? +data : rbrace.test( data ) ? jQuery.parseJSON( data ) : data; } catch( e ) {}
// Make sure we set the data so it isn't changed later // 将该值存入元素的data缓存 jQuery.data( elem, key, data );
} else { data = undefined; } }
return data; } |
接着就是internalData,逻辑比较复杂。在此之前,我们先看一下相关的函数:
// jQuery初始化时,会创建一个随机的key值,这个值将会用于元素的内置jQuery mapping expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),
// 可以接受数据赋值的mapping表 noData: { "applet ": true, "embed ": true, // ...but Flash objects (which have this classid) *can* handle expandos "object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" },
/** * Determines whether an object can have data */ jQuery.acceptData = function( elem ) { // 检查元素类型是否接受数据赋值 var noData = jQuery.noData[ (elem.nodeName + " ").toLowerCase() ], // 获取nodeType,如果没有(js object等)则将其认为是dom元素 nodeType = +elem.nodeType || 1;
// 对元素类型进行判断,dom元素或者document或者flash object可以接受数据赋值 // Do not set data on non-element DOM nodes because it will not be cleared (#8335). return nodeType !== 1 && nodeType !== 9 ? false :
// Nodes accept data unless otherwise specified; rejection can be conditional !noData || noData !== true && elem.getAttribute("classid") === noData; }; |
然后是internalData:
function internalData( elem, name, data, pvt /* Internal Use Only */ ) { // 元素是否接受数据赋值(见上) if ( !jQuery.acceptData( elem ) ) { return; }
var ret, thisCache, internalKey = jQuery.expando,//见上
// jQuery会分开处理js object和dom元素的数据赋值(因为IE 6-7有GC bug) // We have to handle DOM nodes and JS objects differently because IE6-7 // can't GC object references properly across the DOM-JS boundary isNode = elem.nodeType,
// Only DOM nodes need the global jQuery cache; JS object data is // attached directly to the object so GC can occur automatically // 重点!如果是dom元素则使用jQuery的cache( jQuery使用global cache来保存dom元素赋值的数据) cache = isNode ? jQuery.cache : elem,
// Only defining an ID for JS objects if its cache already exists allows // the code to shortcut on the same path as a DOM node with no cache // 获取元素内部id,如果不是dom元素则直接以internalKey作为id id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;
// Avoid doing any more work than we need to when trying to get data on an // object that has no data at all if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) { return; }
// 如果没有id则赋予id if ( !id ) { // Only DOM nodes need a new unique ID for each element since their data // ends up in the global cache if ( isNode ) { // jQuery内置了一个deletedIds数组用于存储被弃用的id,如果有弃用的id则会被复用 id = elem[ internalKey ] = deletedIds.pop() || jQuery.guid++; } else { id = internalKey; } }
// 如果没有cache则创建。对于js object,会添加一个toJSON的空方法以阻住JSON.stringify将容器内数据一同转换 if ( !cache[ id ] ) { // Avoid exposing jQuery metadata on plain JS objects when the object // is serialized using JSON.stringify cache[ id ] = isNode ? {} : { toJSON: jQuery.noop }; }
// 允许直接传入一个object将其数据添加到cache // An object can be passed to jQuery.data instead of a key/value pair; this gets // shallow copied over onto the existing cache 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 ];
// jQuery的缓存结构:{data: {}},用户存储的数据会被存于data中,从而避免和jQuery保存的数据冲突 // jQuery data() is stored in a separate object inside the object's internal data // cache in order to avoid key collisions between internal data and user-defined // data. if ( !pvt ) { if ( !thisCache.data ) { thisCache.data = {}; }
thisCache = thisCache.data; }
// 保存数据,同样会使用驼峰法将key改写 if ( data !== undefined ) { thisCache[ jQuery.camelCase( name ) ] = data; }
// 获取数据,会返回原key的值,如果没有则返回驼峰法后key的值。如果key不是string类型,直接返回所有赋值数据 // Check for both converted-to-camel and non-converted data property names // If a data property was specified if ( typeof name === "string" ) {
// First Try to find as-is property data ret = thisCache[ name ];
// Test for null|undefined property data if ( ret == null ) {
// Try to find the camelCased property ret = thisCache[ jQuery.camelCase( name ) ]; } } else { ret = thisCache; }
return ret; } |
我们看一下简化的流程图:
看过了jQuery的数据赋值思路后,我们也可以实现一个简单的版本:
var cache = []; var _innerID = 0;
function data(ele, key, value) { if(!ele._innerID) { ele._innerID = ++_innerID; }
var _cache = cache[ele._innerID] = cache[ele._innerID] || {};
if(key === undefined) { return _cache; } else if(value === undefined) { return _cache[key]; } else { _cache[key] = value; } } |
总结
jQuery数据赋值的实现分成dom元素和非dom元素两种。两者除了缓存容器选取不同外,存取逻辑是公用的。其中对于dom元素,会额外处理一次html5的[data-*]数据赋值。但是就如前所说的,当data-*数据被存入缓存容器后,jQuery便不会再次处理。因而通过data-*更新的数据将不会被$.fn.data获取到。
因而,我建议除非由于页面初始化赋值,否则应该尽量避免动态调整[data-*]赋值,而转用jQuery的data方法存取赋值。