Skip to content

Commit 1d5d959

Browse files
committed
Optimized Data rewrite
1 parent 6a0ee2d commit 1d5d959

File tree

2 files changed

+99
-64
lines changed

2 files changed

+99
-64
lines changed

src/data.js

Lines changed: 92 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,108 @@
1+
/*
2+
Implementation Summary
3+
4+
1. Enforce API surface and semantic compatibility with 1.9.x branch
5+
2. Improve the module's maintainability by reducing the storage
6+
paths to a single mechanism.
7+
3. Use the same single mechanism to support "private" and "user" data.
8+
4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData)
9+
5. Avoid exposing implementation details on user objects (eg. expando properties)
10+
6. Provide a clear path for implementation upgrade to WeakMap in 2014
11+
*/
112
var data_user, data_priv,
213
rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/,
314
rmultiDash = /([A-Z])/g;
415

516
function Data() {
6-
// Nodes|Objects
7-
this.owners = [];
8-
// Data objects
9-
this.cache = [];
17+
// Data objects. Keys correspond to the
18+
// unlocker that is accessible via "locker" method
19+
this.cache = {};
1020
}
1121

12-
Data.index = function( array, node ) {
13-
return array.indexOf( node );
14-
};
15-
22+
Data.uid = 1;
1623

1724
Data.prototype = {
18-
add: function( owner ) {
19-
return (this.cache[ this.owners.push( owner ) - 1 ] = {});
25+
locker: function( owner ) {
26+
var ovalueOf,
27+
// Check if the owner object has already been outfitted with a valueOf
28+
// "locker". They "key" is the "Data" constructor itself, which is scoped
29+
// to the IIFE that wraps jQuery. This prevents outside tampering with the
30+
// "valueOf" locker.
31+
unlock = owner.valueOf( Data );
32+
33+
// If no "unlock" string exists, then create a valueOf "locker"
34+
// for storing the unlocker key. Since valueOf normally does not accept any
35+
// arguments, extant calls to valueOf will still behave as expected.
36+
if ( typeof unlock !== "string" ) {
37+
unlock = jQuery.expando + Data.uid++;
38+
ovalueOf = owner.valueOf;
39+
40+
Object.defineProperty( owner, "valueOf", {
41+
value: function( pick ) {
42+
if ( pick === Data ) {
43+
return unlock;
44+
}
45+
return ovalueOf.apply( owner );
46+
}
47+
// By omitting explicit [ enumerable, writable, configurable ]
48+
// they will default to "false"
49+
});
50+
}
51+
52+
// If private or user data already create a valueOf locker
53+
// then we'll reuse the unlock key, but still need to create
54+
// a cache object for this instance (could be private or user)
55+
if ( !this.cache[ unlock ] ) {
56+
this.cache[ unlock ] = {};
57+
}
58+
59+
return unlock;
2060
},
2161
set: function( owner, data, value ) {
22-
var prop,
23-
index = Data.index( this.owners, owner );
24-
25-
// If there is no entry for this "owner", create one inline
26-
// and set the index as though an owner entry had always existed
27-
if ( index === -1 ) {
28-
this.add( owner );
29-
index = this.owners.length - 1;
30-
}
62+
var prop, cache, unlock;
63+
64+
// There may be an unlock assigned to this node,
65+
// if there is no entry for this "owner", create one inline
66+
// and set the unlock as though an owner entry had always existed
67+
unlock = this.locker( owner );
68+
cache = this.cache[ unlock ];
69+
3170
// Handle: [ owner, key, value ] args
3271
if ( typeof data === "string" ) {
33-
this.cache[ index ][ data ] = value;
72+
cache[ data ] = value;
3473

3574
// Handle: [ owner, { properties } ] args
3675
} else {
37-
// In the case where there was actually no "owner" entry and
38-
// this.add( owner ) was called to create one, there will be
76+
// [*] In the case where there was actually no "owner" entry and
77+
// this.locker( owner ) was called to create one, there will be
3978
// a corresponding empty plain object in the cache.
40-
if ( jQuery.isEmptyObject( this.cache[ index ] ) ) {
41-
this.cache[ index ] = data;
42-
79+
//
80+
// Note, this will kill the reference between
81+
// "this.cache[ unlock ]" and "cache"
82+
if ( jQuery.isEmptyObject( cache ) ) {
83+
cache = data;
4384
// Otherwise, copy the properties one-by-one to the cache object
4485
} else {
4586
for ( prop in data ) {
46-
this.cache[ index ][ prop ] = data[ prop ];
87+
cache[ prop ] = data[ prop ];
4788
}
4889
}
4990
}
91+
92+
// [*] This is required to support an expectation made possible by the old
93+
// data system where plain objects used to initialize would be
94+
// set to the cache by reference, instead of having properties and
95+
// values copied.
96+
this.cache[ unlock ] = cache;
97+
5098
return this;
5199
},
52100
get: function( owner, key ) {
53-
var cache,
54-
index = Data.index( this.owners, owner );
55-
56-
// A valid cache is found, or needs to be created.
57-
// New entries will be added and return the current
58-
// empty data object to be used as a return reference
59-
// return this.add( owner );
60-
// This logic was required by expectations made of the
61-
// old data system.
62-
cache = index === -1 ?
63-
this.add( owner ) : this.cache[ index ];
101+
// Either a valid cache is found, or will be created.
102+
// New caches will be created and the unlock returned,
103+
// allowing direct access to the newly created
104+
// empty data object.
105+
var cache = this.cache[ this.locker( owner ) ];
64106

65107
return key === undefined ?
66108
cache : cache[ key ];
@@ -87,9 +129,8 @@ Data.prototype = {
87129
},
88130
remove: function( owner, key ) {
89131
var i, l, name,
90-
camel = jQuery.camelCase,
91-
index = Data.index( this.owners, owner ),
92-
cache = this.cache[ index ];
132+
unlock = this.locker( owner ),
133+
cache = this.cache[ unlock ];
93134

94135
if ( key === undefined ) {
95136
cache = {};
@@ -98,14 +139,12 @@ Data.prototype = {
98139
// Support array or space separated string of keys
99140
if ( !Array.isArray( key ) ) {
100141
// Try the string as a key before any manipulation
101-
//
102-
103142
if ( key in cache ) {
104143
name = [ key ];
105144
} else {
106145
// If a key with the spaces exists, use it.
107146
// Otherwise, create an array by matching non-whitespace
108-
name = camel( key );
147+
name = jQuery.camelCase( key );
109148
name = name in cache ?
110149
[ name ] : ( name.match( core_rnotwhite ) || [] );
111150
}
@@ -116,7 +155,7 @@ Data.prototype = {
116155
// Since there is no way to tell _how_ a key was added, remove
117156
// both plain key and camelCase key. #12786
118157
// This will only penalize the array argument path.
119-
name = key.concat( key.map( camel ) );
158+
name = key.concat( key.map( jQuery.camelCase ) );
120159
}
121160
i = 0;
122161
l = name.length;
@@ -126,21 +165,15 @@ Data.prototype = {
126165
}
127166
}
128167
}
129-
this.cache[ index ] = cache;
168+
this.cache[ unlock ] = cache;
130169
},
131170
hasData: function( owner ) {
132-
var index = Data.index( this.owners, owner );
133-
134-
return index !== -1 && !jQuery.isEmptyObject( this.cache[ index ] );
171+
return !jQuery.isEmptyObject(
172+
this.cache[ this.locker( owner ) ]
173+
);
135174
},
136175
discard: function( owner ) {
137-
var index = Data.index( this.owners, owner );
138-
139-
if ( index !== -1 ) {
140-
this.owners.splice( index, 1 );
141-
this.cache.splice( index, 1 );
142-
}
143-
return this;
176+
delete this.cache[ this.locker( owner ) ];
144177
}
145178
};
146179

@@ -159,14 +192,15 @@ data_priv = new Data();
159192

160193

161194
jQuery.extend({
195+
// Unique for each copy of jQuery on the page
196+
// Non-digits removed to match rinlinejQuery
197+
expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ),
198+
162199
// This is no longer relevant to jQuery core, but must remain
163200
// supported for the sake of jQuery 1.9.x API surface compatibility.
164201
acceptData: function() {
165202
return true;
166203
},
167-
// Unique for each copy of jQuery on the page
168-
// Non-digits removed to match rinlinejQuery
169-
expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ),
170204

171205
hasData: function( elem ) {
172206
return data_user.hasData( elem ) || data_priv.hasData( elem );
@@ -245,7 +279,6 @@ jQuery.fn.extend({
245279
if ( data !== undefined ) {
246280
return data;
247281
}
248-
249282
// Attempt to "discover" the data in
250283
// HTML5 custom data-* attrs
251284
data = dataAttr( elem, key, undefined );
@@ -320,6 +353,5 @@ function dataAttr( elem, key, data ) {
320353
data = undefined;
321354
}
322355
}
323-
324356
return data;
325357
}

test/unit/data.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,17 +126,20 @@ test("jQuery.data(document)", 25, function() {
126126
QUnit.expectJqData(document, "foo");
127127
});
128128

129+
130+
/*
131+
// Since the new data system does not rely on expandos, limiting the type of
132+
// nodes that can have data is no longer necessary. jQuery.acceptData is now irrelevant
133+
// and should eventually be removed from the library.
134+
129135
test("Data is not being set on comment and text nodes", function() {
130136
expect(2);
131137
132138
ok( !jQuery.hasData( jQuery("<!-- comment -->").data("foo", 0) ) );
133139
ok( !jQuery.hasData( jQuery("<span>text</span>").contents().data("foo", 0) ) );
134140
135141
});
136-
/*
137-
// Since the new data system does not rely on exandos, limiting the type of
138-
// nodes that can have data is no longer necessary. jQuery.acceptData is now irrelevant
139-
// and should be removed from the library.
142+
140143
141144
test("jQuery.acceptData", function() {
142145
expect(9);

0 commit comments

Comments
 (0)