问题1:jQuery内部的数据保存的格式是什么,通过data保存的数据和注册的事件格式有什么区别?
首先看下面的代码:
$._data($("#data")[0],"name","qinliang"); $._data($("#data")[0],"sex","male"); $("#data").click(function() { console.log("click1"); }); $("#data").click(function() { console.log("click2"); }); $("#data").mouseover(function() { console.log("click"); }); //data保存的数据通过result.name,result.sex访问 //在DOM上面绑定的事件函数如果要访问,可以通过result.events.click,result.events.mouseover不过返回的都是数组 //特别注意:这里是通过_data方法保存的数据! var result=$._data($("#data")[0]); console.log(result);
note:通过该图片我们知道,通过_data保存的数据是保存在DOM对象上面的,通过$._data(document.getElementById("data"))获取到的对象通过键名就可以访问到;但是对于事件绑定的函数,只能通过$._data(document.getElementById("data")).events["click"]才能访问。
问题2:用户通过$.data保存的数据格式是如何的?
我们修改上面的代码为:
$.data($("#data")[0],"name","qinliang"); $.data($("#data")[0],"sex","male"); $("#data").click(function() { console.log("click1"); }); $("#data").click(function() { console.log("click2"); }); $("#data").mouseover(function() { console.log("click"); }); //data保存的数据通过result.name,result.sex访问 //在DOM上面绑定的事件函数如果要访问,可以通过result.events.click,result.events.mouseover不过返回的都是数组 //特别注意:这里是通过_data方法保存的数据! var result=$.data($("#data")[0]); console.log(result);note:通过该 图,你会发现用户保存的数据,也就是通过$.data方法保存的数据只会保存自定义数据,而像事件注册等属于内部的数据,这部分数据不在用户数据范围之内!
解答:像事件event注册或者_data是通过jQuery内部调用的,保存的数据格式就是第一张图,而用户通过data保存的数据是第二张图的格式,这种格式中只包含用户自定义的数据!
问题4:我们在jQuery实例上绑定的数据在底层是如何保存的?和通过jQuery保存数据格式是否一样?
我们把上面的例子修改为实例方法:
$("#data").data("name","qinliang"); $("#data").data("sex","male"); $("#data").click(function() { console.log("click1"); }); $("#data").click(function() { console.log("click2"); }); $("#data").mouseover(function() { }) //先获取jQuery.expando才能去获取钥匙,所以jQuery.expando是最重要的,他是打开大门的第一步! var internalKey=jQuery.expando; //通过jQuery.expando去获取钥匙 var id= $("#data")[0][internalKey]; //获取到钥匙以后,就可以去jQuery.cache这个仓库去获取到数据了! var result=jQuery.cache[id]; //获取到的数据 console.log(result);note:通过 该图我们知道,实例对象底层保存的数据格式具有 三个域,分别为data域保存用户的数据;events保存的是对象的事件;handler域就是通用的回调函数!
expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" )//每次保存数据的时候会生产一个expando?,具有随机性?每次保存数据的时候都会产生随机数?其实不是的,他只会在第一次调用的时候产生一次,这一点一定要注意,所以不管你调用多少次data方法,其值都是一样的!
下面的代码,产生的随机数都是一样的.
var result1=jQuery.expando; var result2=jQuery.expando; console.log(result1); console.log(result2);//result1和result2是相等的,因为只在第一次调用的时候产生一次随机数,以后就是直接访问前面的随机数了,因为是全局变量非函数!
note:所有的DOM都是以相同的expando来保存数据的,也就是每一个DOM都有相同的属性名jQuery.expando!
问题5:既然expando是唯一的,那么怎么使得每一个DOM都有不同的钥匙?if ( isNode ) { id = elem[ internalKey ] = deletedIds.pop() || jQuery.guid++; } else { id = internalKey; }note:这一段代码告诉我们, 虽然属性名是相同的,但是属性值是不一样的,而且是通过jQuery.guid来保证唯一性的。
问题6:jQuery.guid是什么鬼?他为啥能够保证每一个DOM元素具有不重复的钥匙?
proxy函数产生新的函数的时候会处理guid,让他的值递增:
proxy: function( fn, context ) { var args, proxy, tmp; if ( typeof context === "string" ) { tmp = fn[ context ]; context = fn; fn = tmp; } // Quick check to determine if target is callable, in the spec // this throws a TypeError, but we will just return undefined. if ( !jQuery.isFunction( fn ) ) { return undefined; } // Simulated bind args = slice.call( arguments, 2 ); proxy = function() { return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); }; // Set the guid of unique handler to the same of original handler, so it can be removed proxy.guid = fn.guid = fn.guid || jQuery.guid++;//对新的函数设置guid++保证每一个通过jQuery处理的函数都有一个guid值,而且唯一! return proxy; }jQuery.event.add方法中,对每一个绑定的函数都会设置一个新的guid,前提是他没有guid值:
if ( !handler.guid ) {//没有guid值添加 handler.guid = jQuery.guid++; }在实例on函数中,如果一个函数只会被调用一次,那么设置guid值(其实就是proxy函数)
if ( one === 1 ) { origFn = fn; fn = function( event ) { // Can use an empty set, since event contains the info jQuery().off( event ); return origFn.apply( this, arguments ); }; // Use same guid so caller can remove using origFn fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );//为函数都唯一生产guid,其它函数通过调用上面的jQuery.event.add保证唯一! }实例保存数据的时候,为每一个DOM对象的相同的属性,即jQuery.expando属性设置一个唯一的钥匙值:
if ( isNode ) { id = elem[ internalKey ] = deletedIds.pop() || jQuery.guid++; } else { id = internalKey; }note:j Query.guid的作用是用于事件绑定中和数据保存中,为每一个保存的数据的DOM和每一个绑定事件的回调函数分配一个唯一的guid值!
jQuery.cache={}, cache = isNode ? jQuery.cache : elem,从这里我们知道,如果是DOM对象,那么仓库就是jQuery.cache,如果是js对象那么仓库就是自身!
上面我们说过:实例对象底层保存的数据格式具有三个域,分别为data域保存用户的数据;events保存的是对象的事件;handler域就是通用的回调函数!那么我的数据是如何保存到data域下面的:
if ( !cache[ id ] ) {//如果DOM的仓库还没存在,也就是没调用过data方法,那么我们初始化仓库! // Avoid exposing jQuery metadata on plain JS objects when the object // is serialized using JSON.stringify cache[ id ] = isNode ? {} : { toJSON: jQuery.noop }; } // 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" ) {//如果是object或者function那么我们让仓库继承他所有的属性 if ( pvt ) { cache[ id ] = jQuery.extend( cache[ id ], name );//jQuery内部的数据直接保存在仓库里面 } else { cache[ id ].data = jQuery.extend( cache[ id ].data, name );//如果是自定义数据保存在仓库的data域下面! } }note:用户数据保存在仓库的data域下面,如果是jQuery数据保存在仓库里就可以了,不用重新开辟一个空间!
//获取到数据仓库,cache是仓库,id是钥匙! thisCache = cache[ id ]; // 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. //如果是用户数据,而且没有data域,那么开启data域 if ( !pvt ) { if ( !thisCache.data ) { thisCache.data = {}; } thisCache = thisCache.data; } //到达这里,如果是用户数据已经获取到仓库的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 //如果name是string那么获取仓库中数据返回,否则直接返回仓库本身! 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;note:如果data存在,表示是保存数据,这时候把数据保存到特定的域下面。用户数据在仓库的data域下面,内部数据直接在仓库里面!;如果是获取数据,如果name是string那么获取仓库中特定的内容,否则直接返回仓库本身!
var obj=new Object();//在object对象上面保存数据! jQuery.data(obj,"name","qinliang"); jQuery.data(obj,"sex","female"); var result=jQuery.data(obj,"sex"); console.log(result);//打印sexnote:这种保存数据的方式不是DOM触发的,而是JS对象完成的。然而data方法属于jQuery对象, 所以无法直接调用data方法,但是可以调用jQuery.data方法!这种方式也可以在Object对象上面保存数据!我们来看看这种方式保存数据格式是怎么样的:
var obj=new Object(); jQuery.data(obj,"name","qinliang"); var key=jQuery.expando; //对于Object对象,钥匙就是jQuery.expando var warehouse=obj; //获取仓库,Object对象的仓库是自身 var data= warehouse[key]; //获取数据 console.log(data);note:通过该 图你会发现,通过object对象保存的数据也是在data域下面,同时为了保证数据的安全性,该元素的toJson方法是空的,也表示无法对数据进行json格式化!
//内部数据是只能通过jQuery._data自己调用 jQuery._data($("#data")[0],"name","qinliang"); jQuery._data($("#data")[0],"sex","male"); $("#data").click(function() { console.log("1"); }); //获取expando以求获取钥匙 var expando=jQuery.expando; //获取钥匙 var key=$("#data")[0][expando]; //获取仓库 var walhouse=jQuery.cache[key]; console.log(walhouse);note:通过该 图我们再次确认了, jQeury内部的数据都是单独保存的,没有放入data域下面进行重新组织,这一点一定要和用户自定义的数据区分开来!
jQuery内部数据通过jQuery._data方法保存
_data: function( elem, name, data ) { return internalData( elem, name, data, true );//第四个参数为true表示保存在非data域下面! }用户数据保存在data域下面:
data: function( elem, name, data ) { return internalData( elem, name, data ); }问题13:我们调用实例方法如果需要设置多个属性和值,那么必须多次调用data方法?
解答:不是的,我们看看实例data方法是如何处理的
<span style="font-size:10px;">if ( typeof key === "object" ) { return this.each(function() { jQuery.data( this, key );//这说明我们如果要保存多个值,那么可以传入对象字面量 }); }</span>那么我们在看看jQuery.data是如何支持实例方法传入对象字面量的
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 ); } }note:jQuery.data支持对象字面量是通过jQuery.extend来实现的,他会把我们通过data实例方法传入的对象字面量全部继承到仓库中!
解答:不对,我们看看data实例方法是如何处理的
f ( key === undefined ) { if ( this.length ) { //获取第1个元素的数据 data = jQuery.data( elem ); //如果第1个调用元素的parsedAttrs属性为空! 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) ); //通过dataAttr方法获取到HTML5数据 //传入如dataAttr(elem,"sex",undefined) dataAttr( elem, name, data[ name ] ); } } } jQuery._data( elem, "parsedAttrs", true ); } } return data; }note:如果没有传入参数,那么首先获取第一个DOM元素的所有的属性,同时查找该DOM元素上同名的HTML5属性,把这个数据保存到jQuery内部数据中,键名是parsedAttrs!这一点是jQuery智能操作完成的!看下面的例子:
HTML部分:
<div style="width:100px;height:100px;background-color:red;" id="data" data-sex="male" data-name="qinliang"></div>JS部分:
//这时候传入的参数是undefined,所以最后就是把HTML5的数据保存为用户的数据,键名为HTML5键名的 //缩写版,如"sex"而不是"data-sex"。键值就是HTML5数据的值! var result=$("#data").data(); var result1=$("#data").data("name"); console.log(result1);//打印qinliang var result2=$("#data").data("sex"); console.log(result2);//打印malenote: 在这里我只想说,如果用户调用data方法时候没有传入任何参数,那么jQuery会把HTML5数据作为用户数据保存,所以下次再次获取数据的时候就可以获取到了!
function dataAttr( elem, key, data ) { // If nothing was found internally, try to fetch any // data from the HTML5 data-* attribute //如果传入的data是undefined,同时elemenet是DOM对象,那么我们就获取HTML5上的同名属性! if ( data === undefined && elem.nodeType === 1 ) { //如果是大写就满足这个正则表达式,那么会添加两个横线并且变成小写! var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); data = elem.getAttribute( name ); if ( typeof data === "string" ) { try { //如果是true,false,null字符串那么返回非字符串形式的数据,如果不是这几种类型 data = data === "true" ? true : data === "false" ? false : data === "null" ? null : // Only convert to a number if it doesn't change the string //如果是数字那么才会转化为数子,如果是JSON类型那么转化为JSON,否则就是原样返回! +data + "" === data ? +data : rbrace.test( data ) ? jQuery.parseJSON( data ) : data; } catch( e ) {} //把我们获取到的数据保存为用户数据 // Make sure we set the data so it isn't changed later jQuery.data( elem, key, data ); } else { data = undefined; } } return data; }note:我相信你不难看出,最后是通过jQuery.data把数据保存为用户数据的!注意,我们现在回到上面的那个不传入参数的情况,那么他其实是只保存了调用对象的第1个元素的数据作为用户数据,如果有多个DOM元素组成了调用对象,剩余的DOM对象是没有数据的,看下面的例子:
HTML部分
<div style="width:100px;height:100px;background-color:red;" class="data" data-sex="male" data-name="qinliang"></div> <div style="width:100px;height:100px;background-color:red;" class="data" id="me"></div>JS部分:
//这时候传入的参数是undefined,所以最后就是把HTML5的数据保存为用户的数据,键名为HTML5键名的 //缩写版,如"sex"而不是"data-sex"。键值就是HTML5数据的值! var result=$(".data").data(); var result1=$(".data").data("name"); console.log(result1);//打印qinliang var result2=$(".data").data("sex"); console.log(result2);//打印male var result=$("#me").data(); var result1=$("#me").data("name"); console.log(result1);//打印undfined var result2=$("#me").data("sex"); console.log(result2);//打印undefinednote:虽然第一步$(".data")匹配了多个DOM元素, 但是只是把第一个DOM元素的HTML5属性作为第一个DOM元素的内部数据,所以第二个DOM元素,也就是id为me的元素仍然没有name和sex属性值!
问题15:实例data源码中有parsedAttrs,是干嘛用的?
如果我们调用data方法但是没有传入参数,这时候就会存在parsedAttrs了
jQuery._data( elem, "parsedAttrs", true );//作为jQuery内部数据
其值作为内部数据保存
//这时候传入的参数是undefined,所以最后就是把HTML5的数据保存为用户的数据,键名为HTML5键名的 //缩写版,如"sex"而不是"data-sex"。键值就是HTML5数据的值! var result=$(".data").data(); //获取expando以备获取钥匙 var expando=jQuery.expando; //获取钥匙 var key=$(".data")[0][expando]; //获取仓库 var walhouse=jQuery.cache; //打开仓库获取数据 var result=walhouse[key]; console.log(result);note:通过 该图你要知道parseAttrs保存的是true,表示已经解析过HTML5属性了;但是注意:内部数据和外部数据虽然获取的方式是一样的,都是获取expando,获取钥匙,获取仓库,获取数据。 但是你应该马上反应过来,上面的parseAttrs没有保存到data域下面,这就是他通过内部数据保存的结果。同时,你也应该看到内部数据和用户自定义数据在这个图上存在的共存的情况!
问题16:data如何体现了获取数据的逻辑?
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 //如果传入的参数不大于1,也就是传入单个值,因为不传值上面已经return了 //那么用dataAttr获取值,传入参数为第一个DOM对象,第二个参数是data方法传入的参数 //第三个参数是第一个DOM对象的所有通过$.data保存的数据 elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : undefined; }note:jQuery.data(elem,key)就是获取元素上指定键名的键值数据,如果键值存在不是undefined那么dataAttr不做任何处理,直接返回; 如果键值不存在那么dataAttr就回去获取HTML5属性值,保存为用户数据的同时返回数据!所以下面这种情况是有值返回的。
<div style="width:100px;height:100px;background-color:red;" id="data" data-name="qinliang"></div>JS部分:
var result=$("#data").data("name"); console.log(result);note:用户没有保存数据的情况下我们却获取到了数据,这就是dataAttr的功劳,他把数据保存为 用户数据的同时把数据返回。
看看筛选函数
if ( !jQuery.acceptData( elem ) ) { return; }详细代码如下
noData: { "applet ": true, "embed ": true, // ...but Flash objects (which have this classid) *can* handle expandos "object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" } jQuery.acceptData = function( elem ) { var noData = jQuery.noData[ (elem.nodeName + " ").toLowerCase() ], nodeType = +elem.nodeType || 1; // Do not set data on non-element DOM nodes because it will not be cleared (#8335). //如果元素不是Element同时也不是document那么直接返回false表示不能用来保存数据! return nodeType !== 1 && nodeType !== 9 ? false : //如果是元素或者为document对象可以保存数据;applet和embed不能保存数据;Flash可以保存数据! // Nodes accept data unless otherwise specified; rejection can be conditional !noData || noData !== true && elem.getAttribute("classid") === noData; };note:Element对象,document对象,Flash对象可以保存数据,applet和embed不能保存数据! 很显然JS对象是可以保存数据的,因为里面没有针对JS对象排除的代码!
hasData: function( elem ) { //如果是DOM对象,那么就是判断仓库是否为空的,不需要特别针对data域,elem[jQuery.expando] //是钥匙,jQuery.cache是仓库。如果是JS对象等其它对象,那么钥匙就是jQuery.expando,仓库就是自身! elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; //仓库非空,通过isEmptyObject来判断 return !!elem && !isEmptyDataObject( elem ); }详细见isEmptyDataObject
function isEmptyDataObject( obj ) { var name; for ( name in obj ) { // if the public data object is empty, the private is still empty //如果data域是空的,那么继续判断下一个字段 if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { continue; } //如果有一个除了toJSON的字段,那么表示非空! if ( name !== "toJSON" ) { return false; } } return true; }note:如果有一个除了toJSON的字段,那么表示这个数据对象不是空对象!
jQuery.data方法源码
data: function( elem, name, data ) { return internalData( elem, name, data ); }jQuery._data方法源码
_data: function( elem, name, data ) { return internalData( elem, name, data, true ); }实例data方法源码
data: function( key, value ) { var i, name, data, //获取第1个调用元素 elem = this[0], //保存第一个调用元素的属性集合! attrs = elem && elem.attributes; // Special expections of .data basically thwart jQuery.access, // so implement the relevant behavior ourselves // Gets all values //没有传入key或者key为undefined if ( key === undefined ) { if ( this.length ) { //获取第1个元素的数据 data = jQuery.data( elem ); //如果第1个调用元素的parsedAttrs属性为空! 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) ); //通过dataAttr方法获取到HTML5数据 //传入如dataAttr(elem,"sex",undefined) dataAttr( elem, name, data[ name ] ); } } } jQuery._data( elem, "parsedAttrs", true ); } } return data; } // 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; }internalRemoveData源码:
function internalData( elem, name, data, pvt /* Internal Use Only */ ) { if ( !jQuery.acceptData( elem ) ) { return; } var ret, thisCache, internalKey = jQuery.expando, // 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 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 = 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; } 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 ] ) { // Avoid exposing jQuery metadata on plain JS objects when the object // is serialized using JSON.stringify cache[ id ] = isNode ? {} : { toJSON: jQuery.noop }; } // 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() 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; } 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" ) { // 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.data保存数据和获取数据严格遵守下面的次序:
第一步:获取expando以备用他来获取钥匙。expando=jQuery.expando;
第二步:通过expando获取到钥匙。JS对象:key=expando; DOM对象key=elem[expando]
第三步:获取仓库。JS对象walhouse=elem;DOM对象walhouse=jQuery.cache
第四步:用钥匙打开仓库。JS对象data=elem[expando],DOM对象data=walhouse[key]
第五步:获取数据,如果是用户数据要在data的data域下面,如果是内部数据不用到data域下面!