jQuery源码阅读11:数据缓存data()

读event之前, 核心方法里还有data和queue没读,先把基础方法里的障碍扫清吧

1:data可以把目标数据绑定在dom元素上,或者js对象里
(1).对于dom元素: 程序生成一个uuid关联数据和缓存, 把id写进对象, 把要缓存的数据缓存在jQuery.cache对象里
(2).对于js对象:直接在此对象中增加一个名为jQuery/\d+/的对象, 把要缓存的数据放入此对象
2:缓存机制自身也用data缓存数据,提高效率

这点儿代码看了好几个小时才看明白, 读懂之后发现其实缓存机制道理不复杂 就是流程太多太绕
总感觉对js对象缓存的处理是后加进来的, 做了一大堆追加判断 原有逻辑一下就复杂了
要是我写 有可能写两套逻辑单独处理, 但是那样维护又比较麻烦

所以有感而发:再nb的人写傻大全的代码也避免不了臃肿…

一:用法:缓存数据绑定在dom元素上或js对象里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//1往dom里缓存数据
$("#t1").data("dataObj", { num: 1, blog: "ruantao.duapp.com/blog" });
$("#t1").data("dataObj");//以对象形式返回全部缓存
$("#t1").data("dataObj").blog;//指定的字串
//内部是这么找到的
$.cache[ $("#t1")[0][jQuery.expando] ];
//2往js对象里缓存数据
var obj = {};
jQuery.data(obj,"datat",{ num: 2, blog: "ruantao.duapp.com" });
jQuery.data(obj,"datat");
//内部是这么找到的
obj[jQuery.expando].data;
//obj的数据结构如下:
//Object {jQuery1831004071167088113725: Object}
// -jQuery1831004071167088113725: Object
// --data: Object
// --toJSON: function () {}
// --__proto__: Object
// -__proto__: Object

二:jQuery对象的data()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
/**
* 博文作者:ruantao1989@gmail.com
* 引自博客:ruantao.duapp.com/blog
*/
jQuery.extend({
cache: {},
deletedIds: [],
//关联dom和缓存的编号
uuid: 0,
//唯一标示符, "jQuery"后边跟一串数字, 比如jQuery.expando=>"jQuery1831004071167088113725"
expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ),
// The following elements throw uncatchable exceptions if you
// attempt to add expando properties to them.
noData: {
"embed": true,
// Ban all objects except for Flash (which handle expandos)
"object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",
"applet": true
},
//通过关联id,检查之前是否存有缓存
hasData: function( elem ) {
//如果elem是dom, 就去jQuery.cache里取;
//如果是js对象就去对象,就去此对象中取jQuery-expando对象
elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
//有elem且不为空,返回true
return !!elem && !isEmptyDataObject( elem );
},
data: function( elem, name, data, pvt /* Internal Use Only */ ) {
//不能缓存直接返回
if ( !jQuery.acceptData( elem ) ) {
return;
}
var thisCache, ret,
internalKey = jQuery.expando,
getByName = typeof name === "string",//传入name,就是读取
//判断是否是dom, 官方注释:IE67不能回收添加到dom元素里的循环引用
isNode = elem.nodeType,
//目标是在dom上缓存的,用jQuery.cache,
//不是dom就存在本js对象里
cache = isNode ? jQuery.cache : elem,
//是dom,就 dom[jQuery.expando]查出对应id, 没分配就是undefined
//js对象就是 obj[jQuery.expando] 且 jQuery.expando
id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;
// 是读取,但是有没id没缓存数据,直接return
if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) {
return;
}
//写入之前,没id得分配一个
if ( !id ) {
//dom元素 dom[jQuery.expando]优先用已经删掉的id, 没有的话guid++
if ( isNode ) {
elem[ internalKey ] = id = jQuery.deletedIds.pop() || jQuery.guid++;
} else {
//js对象就直接用jQuery.expando
id = internalKey;
}
}
//jQuery.cache的预处理
if ( !cache[ id ] ) {
//cache里没有,新建一个对象准备缓存
cache[ id ] = {};
//对于javascript对象,
//官方注释: 设置方法toJSON为空函数,以避免在执行JSON.stringify()时暴露缓存数据.如果一个对象定义了方法toJSON(),JSON.stringify()在序列化该对象时会调用这个方法来生成该对象的JSON元素
if ( !isNode ) {
cache[ id ].toJSON = jQuery.noop;
}
}
if ( typeof name === "object" || typeof name === "function" ) {
//对于名字是对象或者函数的,
if ( pvt ) {
//内部
cache[ id ] = jQuery.extend( cache[ id ], name );
} else {
//一般情况: 把name遍历到cache[ id ].data中
cache[ id ].data = jQuery.extend( cache[ id ].data, name );
}
}
//缓存本次要用的对象, 取自jQuery.cache[id]
thisCache = cache[ id ];
//非内部情况下,修改目标对象
if ( !pvt ) {
if ( !thisCache.data ) {
thisCache.data = {};
}
//要使用的对象指向thisCache.data, jQuery.cache数据结构大致如下
// Object {1: Object}
// -1: Object
// --data: Object ==>这个才是目标对象
// --parsedAttrs: true
// --__proto__: Object
// -__proto__: Object
thisCache = thisCache.data;
}
//具体存储实现:
if ( data !== undefined ) {
//存入, key需要转驼峰
thisCache[ jQuery.camelCase( name ) ] = data;
}
//读取模式
if ( getByName ) {
//先去读目标对象的属性
ret = thisCache[ name ];
//有可能读不到
if ( ret == null ) {
//再转成驼峰试试
ret = thisCache[ jQuery.camelCase( name ) ];
}
//写入模式
} else {
ret = thisCache;
}
return ret;
},
removeData: function( elem, name, pvt /* Internal Use Only */ ) {
//没法缓存的直接return
if ( !jQuery.acceptData( elem ) ) {
return;
}
var thisCache, i, l,
isNode = elem.nodeType,//是否是dom元素
//dom用jQuery.cache缓存 ,js对象用自身缓存
cache = isNode ? jQuery.cache : elem,
//dom用自身对象的jQuery.expando属性查出id,
//js对象就用jQuery.expando
id = isNode ? elem[ jQuery.expando ] : jQuery.expando;
//缓存里没有id
if ( !cache[ id ] ) {
return;
}
//传入name,也就是删除指定属性
if ( name ) {
//判断pvt模式
thisCache = pvt ? cache[ id ] : cache[ id ].data;
if ( thisCache ) {
//name可以是数组
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(" ");
}
}
}
//name是数组就得遍历删除
for ( i = 0, l = name.length; i < l; i++ ) {
delete thisCache[ name[i] ];
}
//如果本次要用的缓存对象是空的了, 就算完了
if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) {
return;
}
}
}
// See jQuery.data for more information
if ( !pvt ) {
delete cache[ id ].data;
//pvt要用的父对象都空了, 就是都删完了
if ( !isEmptyDataObject( cache[ id ] ) ) {
return;
}
}
//之前都没return, 就是要删除全部缓存
if ( isNode ) {
jQuery.cleanData( [ elem ], true );
//有个兼容判断是用delete删,还是直接赋null
//Support: IE<9 检测是否能删除附加在DOM Element 上的属性或数据
} else if ( jQuery.support.deleteExpando || cache != cache.window ) {
delete cache[ id ];
} else {
cache[ id ] = null;
}
},
//内部调用的
_data: function( elem, name, data ) {
return jQuery.data( elem, name, data, true );
},
//能否缓存数据
//这个检测看不懂,用noData对象
acceptData: function( elem ) {
var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ];
return !noData || noData !== true && elem.getAttribute("classid") === noData;
}
});

三:fn.data()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
* 博文作者:ruantao1989@gmail.com
* 引自博客:ruantao.duapp.com/blog
*/
jQuery.fn.extend({
data: function( key, value ) {
var parts, part, attr, name, l,
elem = this[0],
i = 0,
data = null;
//$Obj.data()的话,会以对象的形式返回全部缓存数据
if ( key === undefined ) {
//选择器拿到this的长度
if ( this.length ) {
//调用fn.data
data = jQuery.data( elem );
//以下是使用缓存:
//优先获取data自身的缓存,名为"parsedAttrs"
if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) {
//dom对象的全部属性(类数组对象)
attr = elem.attributes;
//遍历属性
for ( l = attr.length; i < l; i++ ) {
name = attr[i].name;
//处理html5形式缓存的数据
if ( !name.indexOf( "data-" ) ) {
//截掉data-,转为驼峰
name = jQuery.camelCase( name.substring(5) );
//兼容html5的: data-*属性
dataAttr( elem, name, data[ name ] );
}
}
//写入data自身的缓存
jQuery._data( elem, "parsedAttrs", true );
}
}
return data;
}
//key如果是对象,就遍历存储或是读取
if ( typeof key === "object" ) {
return this.each(function() {
jQuery.data( this, key );
});
}
//key是非对象,这个截取比较难理解, 查过后发现是为了处理".data(key)"循环调用
//如果是".data(key)"则返回第一个匹配元素的指定名称数据
//若是xxx.yyy的形式, 转成["xxx","yyy"]
parts = key.split( ".", 2 );
//yyy转成".yyy"
parts[1] = parts[1] ? "." + parts[1] : "";
//part=".yyy!"
part = parts[1] + "!";
return jQuery.access( this, function( value ) {
//没value就是读取
if ( value === undefined ) {
//triggerHandler是调用trigger触发事件, "getData.yyy!"这个没闹明白, 等看过事件处理再返回头来看
data = this.triggerHandler( "getData" + part, [ parts[0] ] );
//先去data读,在做html5兼容
if ( data === undefined && elem ) {
data = jQuery.data( elem, key );
data = dataAttr( elem, key, data );
}
return data === undefined && parts[1] ?
this.data( parts[0] ) ://如果是".data(key)"则返回第一个匹配元素的指定名称数据
data;
}
//如果有包含".data(key)"的值, 在里边写为目标值
parts[1] = value;
this.each(function() {
var self = jQuery( this );
self.triggerHandler( "setData" + part, parts );
//触发事件,写入数据
jQuery.data( this, key, value );
self.triggerHandler( "changeData" + part, parts );
});
}, null, value, arguments.length > 1, null, false );
},
removeData: function( key ) {
return this.each(function() {
jQuery.removeData( this, key );
});
}
});

四: html5的data-*兼容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/ //验证{}或者是[]
,rmultiDash = /([A-Z])/g; //大写字母
function dataAttr( elem, key, data ) {
//data[name值截掉"data-"] === undefined, 证明
if ( data === undefined && elem.nodeType === 1 ) {
//组装data-驼峰
var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
//取html5的值
data = elem.getAttribute( name );
//有数据
if ( typeof data === "string" ) {
//这个判断谁写的,ca
try {
//特殊处理"true""false""null"这仨字串
data = data === "true" ? true :
data === "false" ? false :
data === "null" ? null :
//下边这行是数字的检测,正常数字 比如非0开头的, 是===的, 需要转数字
+data + "" === data ? +data :
//有括号就处理json
rbrace.test( data ) ? jQuery.parseJSON( data ) :
//过滤完了,用data
data;
} catch( e ) {}
//写入
jQuery.data( elem, key, data );
} else {
data = undefined;
}
}
return data;
}