jQuery数据缓存$.data 的使用以及源码解析

jQuery vincent 1215℃ 0评论

一、实现原理:

对于DOM元素,通过分配一个唯一的关联id把DOM元素和该DOM元素的数据缓存对象关联起来,关联id被附加到以jQuery.expando的值命名的属性上,数据存储在全局缓存对象jQuery.cache中。在读取、设置、移除数据时,将通过关联id从全局缓存对象jQuery.cache中找到关联的数据缓存对象,然后在数据缓存对象上执行读取、设置、移除操作。

 
对于Javascript对象,数据则直接存储在该Javascript对象的属性jQuery.expando上。在读取、设置、移除数据时,实际上是对Javascript对象的数据缓存对象执行读取、设置、移除操作。
 
为了避免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
// 数据缓存 Data
jQuery.extend({
    // 全局缓存对象
    cache: {},
    // 唯一 id种子
    uuid:0,
    // 页面中每个jQuery副本的唯一标识
    expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ),
    // 是否有关联的数据
    hasData: function(){},
    // 设置、读取自定数据或内部数据
    data: function(elem, name, data, pvt) {},
    // 移除自定义数据或内部数据
    removeData: function(elem, name, pvt) {},
    // 设置、读取内部数据
    _data: function(elem, name, data) {},
    // 是否可以设置数据
    acceptData: function(elem){}
});

jQuery.fn.extend({
    // 设置、读取自定义数据,解析HTML5属性data-
    data: function(key,value){},
    // 移除自定义数据
    removeData: function(key){}
});

// 解析HTML5属性 data-
function dataAttr(elem,key,data){}

// 检查数据缓存对象是否为空
function isEmptyDataObject(obj){}

jQuery.extend({
    // 清空数据缓存对象
    cleanData: function(elems){}
});

 

三、$.data(elem, name, data), $.data(elem, name)

 

$.data(elem, name, data)的使用方法:

 
如果传入参数name, 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
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>jQuery.data demo</title>
        <style>
            div { color: blue;}
            span { color: red;}
        </style>
        <script src="//code.jquery.com/jquery-1.10.2.js"></script>
    </head>
    <body>
        <div>
            The values stored were
            <span></span>
            and
            <span></span>
        </div>
        <script>
            var div = $( "div" )[ 0 ];
            jQuery.data( div, "test", {
                first: 16,
                last: "pizza!"
            });
            $( "span:first" ).text( jQuery.data( div, "test" ).first );
            $( "span:last" ).text( jQuery.data( div, "test" ).last );
        </script>
    </body>
</html>

 

$.data(elem, name)的使用方法:

 
如果传入key, 未传入参数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
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>jQuery.data demo</title>
        <style>
            div { margin: 5px; background: yellow;}
            button { margin: 5px; font-size: 14px;}
            p { margin: 5px; color: blue;}
            span { color: red;}
        </style>
        <script src="//code.jquery.com/jquery-1.10.2.js"></script>
    </head>
    <body>
        <div>A div</div>
        <button>Get "blah" from the div</button>
        <button>Set "blah" to "hello"</button>
        <button>Set "blah" to 86</button>
        <button>Remove "blah" from the div</button>
        <p>The "blah" value of this div is <span>?</span></p>
        <script>
            $( "button" ).click( function() {
                var value,
                    div = $( "div" )[ 0 ];
                switch ( $( "button" ).index( this ) ) {
                    case 0 :
                        value = jQuery.data( div, "blah" );
                        break;
                    case 1 :
                        jQuery.data( div, "blah", "hello" );
                        value = "Stored!";
                        break;
                    case 2 :
                        jQuery.data( div, "blah", 86 );
                        value = "Stored!";
                        break;
                    case 3 :
                        jQuery.removeData( div, "blah" );
                        value = "Removed!";
                        break;
                }
                $( "span" ).text( "" + value );
            });
        </script>
    </body>
</html>

 

$.data(elem, name, data), $.data(elem, name) 源码解析:

 

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
jQuery.extend({
    // 1. 定义jQuery.data(elem, name, data, pvt)
    data: function( elem, name, data, pvt /* Internal Use Only */ ) {

        // 2. 检查是否可以设置数据
        if ( !jQuery.acceptData( elem ) ) {
            return; // 如果参数elem不支持设置数据,则立即返回
        }

        // 3 定义局部变量
        var privateCache, thisCache, ret,
            internalKey = jQuery.expando,
            getByName = typeof name === "string",

        // 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, // elem是否是DOM元素

        // 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, // 如果是DOM元素,为了避免javascript和DOM元素之间循环引用导致的浏览器(IE6/7)垃圾回收机制不起作用,要把数据存储在全局缓存对象jQuery.cache中;对于javascript对象,来及回收机制能够自动发生,不会有内存泄露的问题,因此数据可以查收存储在javascript对象上

        // 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,
        isEvents = name === "events";

        // Avoid doing any more work than we need to when trying to get data on an
        // object that has no data at all
        // 4. 如果是读取数据,但没有数据,则返回
        if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) {
            return;
            // getByName && data === undefined 如果name是字符串,data是undefined, 说明是在读取数据
            // !id || !cache[id] || (!isEvents && !pvt && !cache[id].data 如果关联id不存在,说明没有数据;如果cache[id]不存在,也说明没有数据;如果是读取自动以数据,但cache[id].data不存在,说明没有自定义数据
        }

        // 5. 如果关联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 ) {
                elem[ internalKey ] = id = ++jQuery.uuid; // 对于DOM元素,jQuery.uuid会自动加1,并附加到DOM元素上
            } else {
                id = internalKey; // 对于javascript对象,关联id就是jQuery.expando
            }
        }
   
        // 6. 如果数据缓存对象不存在,则初始化为空对象{}
        if ( !cache[ id ] ) {
            cache[ id ] = {};
   
            // Avoids exposing jQuery metadata on plain JS objects when the object
            // is serialized using JSON.stringify
            if ( !isNode ) {
                cache[ id ].toJSON = jQuery.noop; // 对于javascript对象,设置方法toJSON为空函数,以避免在执行JSON.stringify()时暴露缓存数据。如果一个对象定义了方法toJSON(),JSON.stringify()在序列化该对象时会调用这个方法来生成该对象的JSON元素
            }
        }

        // An object can be passed to jQuery.data instead of a key/value pair; this gets
        // shallow copied over onto the existing cache
        // 7. 如果参数name是对象或函数,则批量设置数据
        if ( typeof name === "object" || typeof name === "function" ) {
            if ( pvt ) {
                cache[ id ] = jQuery.extend( cache[ id ], name ); // 对于内部数据,把参数name中的属性合并到cache[id]中
            } else {
                cache[ id ].data = jQuery.extend( cache[ id ].data, name ); // 对于自定义数据,把参数name中的属性合并到cache[id].data中
            }
        }

        // 8. 如果参数data不是undefined, 则设置单个数据
        privateCache = 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;
        }

        // Users should not attempt to inspect the internal events object using jQuery.data,
        // it is undocumented and subject to change. But does anyone listen? No.
        // 9. 特殊处理events
        if ( isEvents && !thisCache[ name ] ) { // 如果参数name是字符串"events",并且未设置过自定义数据"events",则返回事件婚车对象,在其中存储了事件监听函数。
            return privateCache.events;
        }

        // Check for both converted-to-camel and non-converted data property names
        // If a data property was specified
        //10. 如果参数name是字符串,则读取单个数据
        if ( getByName ) {
            // First Try to find as-is property data
            ret = thisCache[ name ]; // 先尝试读取参数name对应的数据

            // Test for null|undefined property data
            if ( ret == null ) { // 如果未取到,则把参数name转换为驼峰式再次尝试读取对应的数据
                // Try to find the camelCased property
                ret = thisCache[ jQuery.camelCase( name ) ];
            }
        } else { // 11. 如果未传入参数name,data,则返回数据缓存对象
            ret = thisCache;
        }

        return ret;
    },

    // For internal use only.
    _data: function( elem, name, data ) {
        return jQuery.data( elem, name, data, true );
    }
});

 

四、.data(key, value), .data(key)

 

使用方法:

 

1
2
3
4
5
$( "body" ).data( "foo", 52 ); // 传入key, value
$( "body" ).data( "bar", { myType: "test", count: 40 } ); // 传入key, value
$( "body" ).data( { baz: [ 1, 2, 3 ] } ); // 传入key, value
$( "body" ).data( "foo" ); // 52 // 传入key
$( "body" ).data(); // 未传入参数

 
HTML5 data attriubutes:
 

1
2
3
4
5
6
7
<div data-role="page" data-last-value="43" data-hidden="true" data-options='{"name":"John"}'></div>
<script>
    $( "div" ).data( "role" ) === "page";
    $( "div" ).data( "lastValue" ) === 43;
    $( "div" ).data( "hidden" ) === true;
    $( "div" ).data( "options" ).name === "John";
</script>

 

.data(key, value), .data(key) 源码解析

 

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
jQuery.fn.extend({
    // 1. 定义.data(key, value)
    data: function( key, value ) {
        var parts, attr, name,
        data = null;

        // 2. 未传入参数的情况
        if ( typeof key === "undefined" ) {
            if ( this.length ) { // 如果参数key是undefined, 即参数格式是.data(), 则调用方法jQuery.data(elem, name, data, pvt)获取第一个匹配元素关联的自定义数据缓存对象,并返回。
                data = jQuery.data( this[0] );

                if ( this[0].nodeType === 1 && !jQuery._data( this[0], "parsedAttrs" ) ) {
                    attr = this[0].attributes;
                    for ( var i = 0, l = attr.length; i &lt; l; i++ ) {
                        name = attr[i].name;

                        if ( name.indexOf( "data-" ) === 0 ) {
                            name = jQuery.camelCase( name.substring(5) );
                            dataAttr( this[0], name, data[ name ] );
                        }
                    }
                    jQuery._data( this[0], "parsedAttrs", true );
                }
            }

            return data;
            // 3. 参数key 是对象的情况,即参数格式是.data(key),则遍历匹配元素集合,为每个匹配元素调用方法jQuery.data(elem, name, data,pvt)批量设置数据
        } else if ( typeof key === "object" ) {
            return this.each(function() {
                jQuery.data( this, key );
            });
        }

        // 4. 只传入参数key的情况 如果只传入参数key, 即参数格式是.data(key),则返回第一个匹配元素的指定名称数据
        parts = key.split(".");
        parts[1] = parts[1] ? "." + parts[1] : "";

        if ( value === undefined ) {
            data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);

            // Try to fetch any internally stored data first
            if ( data === undefined && this.length ) {
                data = jQuery.data( this[0], key );
                data = dataAttr( this[0], key, data );
            }

            return data === undefined && parts[1] ?
                    this.data( parts[0] ) :
                    data;

        // 5. 传入参数key和value的情况 即参数格式是.data(key, value),则为每个匹配元素设置任意类型的数据,并触发自定义事件setData, changeData
        } else {
            return this.each(function() {
                var self = jQuery( this ),
                    args = [ parts[0], value ];

                self.triggerHandler( "setData" + parts[1] + "!", args );
                jQuery.data( this, key, value );
                self.triggerHandler( "changeData" + parts[1] + "!", args );
            });
        }
    },

    removeData: function( key ) {
        return this.each(function() {
            jQuery.removeData( this, key );
        });
    }
});

// 6. 函数dataAttr(elem, key, data)解析HTML5属性data-
function dataAttr( elem, key, data ) {
    // If nothing was found internally, try to fetch any
    // data from the HTML5 data-* attribute
    // 只有参数data为undefined时,才会解析HTML5属性data-
    if ( data === undefined && elem.nodeType === 1 ) {
        var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
        data = elem.getAttribute( name );

        if ( typeof data === "string" ) {
            try {
                data = data === "true" ? true :
                    data === "false" ? false :
                    data === "null" ? null :
                    jQuery.isNumeric( data ) ? parseFloat( 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;
}

 

五、$.removeData(elem, name),.removeData(key)

 

使用方法:

 

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
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>jQuery.removeData demo</title>
        <style>
            div { margin: 2px; color: blue; }
            span { color: red; }
        </style>
        <script src="//code.jquery.com/jquery-1.10.2.js"></script>
    </head>
    <body>
        <div>value1 before creation: <span></span></div>
        <div>value1 after creation: <span></span></div>
        <div>value1 after removal: <span></span></div>
        <div>value2 after removal: <span></span></div>
        <script>
            var div = $( "div" )[ 0 ];
            $( "span:eq(0)" ).text( "" + $( "div" ).data( "test1" ) ); //