Millet Porridge

English version of https://corvo.myseu.cn

0%

The data-* Cache Pitfall in jQuery

I suggest opening F12 debug mode and pasting into the console to see the output of the code snippets (the blog includes jQuery, so you can use $ directly).

Simple Data Caching

jQuery gives us the power to manipulate the DOM tree, which of course also means we can use the DOM tree for simple data storage — temporarily saving variables, or saving them as JSON strings.

The basic working method is like the code below:

1
2
3
4
5
6
7
8
9
// save
var tmpDiv = $('<div />');
tmpDiv.data('info', '12345');

// effect in the page source:
// <div data-info="12345"></div>

// read
console.log(tmpDiv.data('info'))

This is the most basic operation, and also one that won’t go wrong.

The Second Way

Some of you saw <div data-info="12345"></div> — that’s really just setting an attribute on a div. So why not do this: tmpDiv.attr('data-info', '123')?

Look at the code below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var tmpDiv = $('<div />');
tmpDiv.attr('data-info', '123');

alert("The first data is " + tmpDiv.data('info'));

// So far the data is normal — `123` is correctly obtained

// Set the attribute value again.
tmpDiv.attr('data-info', '456');

// Here it breaks: the value obtained is still `123`, not the just-set `456`.
alert("The second data is " + tmpDiv.data('info'));

// The real value should be obtained like this:
alert("The real data is " + tmpDiv.attr('data-info'));

At this point, if you honestly go back to the first way and don’t modify attr directly, you’ll have no problems in the future. But if you want to explore deeper, keep reading.

Why Does the Inconsistency Occur?

See the documentation here: data-html5. The docs contain this sentence:

Since jQuery 1.4.3, data-* attributes are used to initialize jQuery data. An element’s data-* attributes are retrieved the first time the data() method is invoked upon it, and then are no longer accessed or mutated (all values are stored internally by jQuery).

The data-* attribute can be used to initialize jQuery’s data function; on the first call of data(), the data is stored inside jQuery and no longer accessed.

If you’re quick on the uptake, reading the docs is enough — at least you know that after jQuery has used data-*, it no longer depends on its value and keeps an internal copy.

Or Maybe You Want to Read the Source Code

I hadn’t read jQuery source before and hadn’t used it much; this time I hit a problem — never expected to fall into it. In a fit of pique I read the source, and share it here. The code related to the data function is in src/data.js and src/data/Data.js.

There are a few key pieces of code.

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
Data.prototype = {
// I started reading directly from the get function — it indeed takes from the cache each time.
get: function( owner, key ) {
return key === undefined ?
// key is empty: take all data
this.cache( owner ) :

// key has a value: take only the corresponding data
owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ];
},
...
}

jQuery.fn.extend( {
// When we call data() on tmpDiv, this function is called
data: function( key, value ) {
var i, name, data,
elem = this[ 0 ],
attrs = elem && elem.attributes;

// if ( key === undefined ) key is not defined ...

// if ( typeof key === "object" ) if key is an object ...

// And when we give a key, and the key is anything other than an object,
return access( this, function( value ) {
var data;

// if value does not exist
if ( elem && value === undefined ) {

// 1. re-fetch from the cache
data = dataUser.get( elem, key );
if ( data !== undefined ) {
return data;
}

// 2. cache miss: jQuery looks up `data-*` to set this key
data = dataAttr( elem, key );
if ( data !== undefined ) {
return data;
}

// 3. all efforts tried — give up
return;
}

// value exists: reset the data value
this.each( function() {
// We always store the camelCased key
dataUser.set( this, key, value );
} );
}, null, value, arguments.length > 1, null, true );
},
} );

By now I think you understand: if the cache has a value, data-* will not be re-read. Moreover, jQuery‘s data() and the data-* attribute are two different things — related only at initialization. Afterwards, the two can very well become inconsistent.

How Can They Be Consistent?

In the end, inconsistency really does exist between the two. There are three solutions:

  1. Don’t use the data-* scheme from the start: after all, jQuery itself provides the data() function — there’s simply no need to use data-* to set attributes.

  2. Only use data-*: the benefit is you can inspect the data directly via the DOM, and debugging is indeed fast.

  3. Keep them in sync manually: if you insist on using both together, you should reflect on why you’re courting death like this. It’s not impossible — besides the data function, jQuery has another function called removeData; if you don’t mind the trouble, call it once before each use.