jQuery的数据缓存模块

几乎每一个前端库或者框架都有自己的数据缓存模块,而且这个是框架的基础核心模块。所有的应用模块,都会依赖这个数据缓存模块。jQuery也有自己的数据缓存模块,所以把自己学习的心得分享一下。

属性缓存数据

属性缓存数据,就是使用对象的属性或者element元素的attribute来缓存数据。jQuery(1.x)就是利用这个实现的自己的数据缓存模块的。这种方法也叫属性标记法.

  1. element属性attribute标记法

    这个写过前端代码的都很熟悉,就是在标签的html上加上自定义属性。

    栗子1:

    1
    2
    3
    4
    <!-- element-attribute.html -->
    <div id="demo1" customize="defined by me"></div>
    <button id="getAttrBtn">getAttr</button>
    <button id="setAttrBtn">setAttr</button>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var $ = function (id) {
    return document.getElementById(id);
    };
    var getAttrBtn = $('getAttrBtn');
    var setAttrBtn = $('setAttrBtn');

    /**
    *标签属性标记法
    */
    getAttrBtn.onclick = function () {
    var demoEle = $('demo1'), value = demoEle.getAttribute('customize');
    window.console.log(value);
    demoEle.innerHTML = value;
    };

    setAttrBtn.onclick = function () {
    var demoEle = $('demo1'),value;
    demoEle.setAttribute('customize', 'changed by element attribute');
    value = demoEle.getAttribute('customize');
    window.console.log(value);
    demoEle.innerHTML = value;
    };

    栗子1就是我们通过element标签来保存一些自己的业务数据。

    优点:原生;所有浏览器都支持。

    缺点:只能保存字符串,不能保存对象function,这两种类型。

  2. 对象属性标记法

    对象属性标记,就很好理解了,就是平时我们给JavaScript对象赋值的操作。

    栗子2:

    1
    2
    3
    4
    <!-- element-attribute.html -->
    <div id="demo1" customize="defined by me"></div>
    <button id="getAttrBtn">getAttr</button>
    <button id="setAttrBtn">setAttr</button>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /**
    *对象属性标记法
    */
    getAttrBtn.ondblclick = function () {
    var demoEle = $('demo1'), value = demoEle['customize'];
    window.console.log(value);
    demoEle.innerHTML = value;
    };
    setAttrBtn.ondblclick = function () {
    var demoEle = $('demo1'),value;
    demoEle['customize'] = 'changed by object attribute',value = demoEle['customize'];
    window.console.log(value);
    demoEle.innerHTML = value;
    };

    栗子2就是对象属性标记法,原理就是直接使用浏览器中element(宿主元素)进行数据缓存。

    优点:JavaScript全类型的数据都支持。

    缺点:

    • 浏览器兼容性问题。

    • object,applet,embed等对象是不能使用这种方法,进行属性缓存数据,因为他们不是标准的浏览器中的对象,属于扩展内容。

    • window对象如果使用这种方式,变成了全局变量

    • 内存泄漏:内存泄漏是这种方式的最大威胁。

      1
      2
      3
      4
      5
      6
      7
      // 内存泄漏栗子:
      // 最直接的栗子就是demoEle.dep.el的属性指向demoEle
      var demoEle = $('demo1');
      demoEle.dep = {
      el: demoEle
      };
      // 这样的循环依赖,很难觉察到,所以demoEle.dep和demoEle都得不到回收,造成内存泄漏。

数据缓存模块实现原理

属性缓存数据的两种方法,各有优缺点,但是对象属性缓存数据 已经非常理想了,只要解决一下内存泄漏 这个问题就很实用了。分享一下jQuery的巧妙设计。

jQuery的设计思路:

  • 数据统一管理(数据储存在一个对象 中)
  • 数据不缓存在具体的element对象属性上,而是在element缓存一个jQueryKey,然后通过jQueryKey存取数据。

jQueryKey:是一个字符串值,不存在循环依赖 解决内存泄漏的问题 。

栗子:

1
2
3
4
<!-- data-storage.html -->
<div id="demo1"></div>
<button id="getAttrBtn">getStore</button>
<button id="setAttrBtn">store</button>
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
function isNumeric(obj) {
return !isNaN(parseFloat(obj)) && isFinite(obj);
}
function Model() {}
Model.cacheKey = 'ceche' + (new Date).getTime();
Model.cache = {}; // 用来缓存数据
Model.uuid = 0;

Model.data = function (ele, key, value) {
var cacheKey = Model.cacheKey, indexKey = ele[cacheKey];
if (arguments.length === 2) {
// get
if (isNumeric(indexKey)) {
return Model.cache[indexKey][key] === undefined ? null
: Model.cache[indexKey][key];
} else {
return null;
}
} else {
// set
if (isNumeric(indexKey)) {
Model.cache[indexKey][key] = value;
} else {
ele[cacheKey] = indexKey = '' + Model.uuid++;
Model.cache[indexKey] = {};
Model.cache[indexKey][key] = value;
}
}
};

上面的栗子:

Model.cacheKey做element的属性,element[Model.cacheKey]的value值是Model.cache的对象key。

  • element[Model.cacheKey]的value值,只是一个字符串所以,完美解决了内存泄漏 问题。
  • Model.cache是一个普通的JavaScript对象,所以它可以支持所有的JavaScript的数据类型。解决了在标签的属性上缓存数据,只支持 字符串 的问题。
1
2
// 这就是element对应cache中缓存的整个数据对象
Model.cache[element[Model.cacheKey]];

jQuery数据缓存模块源码

jQuery数据缓存模块的部分源码。

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
// 下面的就是一些 jQuery 涉及数据存储的操作
jQuery.extend({
// 全局的缓存对象
cache: {},

// The following elements throw uncatchable exceptions if you
// attempt to add expando properties to them.
// 如果你尝试给以下元素添加扩展属性,将抛出“无法捕捉”的异常
// 这里声明的几个元素对象是不给于数据绑定的
// applet、embed 和 object 元素是不支持设置 expando 属性的,所以不支持 data 方法
noData: {
"applet": true,
"embed": true,
// Ban all objects except for Flash (which handle expandos)
"object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
},

// 检查对象是否已经存储了数据
hasData: function(elem) {
elem = elem.nodeType ? jQuery.cache[elem[jQuery.expando]] : elem[jQuery.expando];
return !!elem && !isEmptyDataObject(elem);
},

// 给 elem(可是DOM,可以是JS对象)添加 key-value 为 name-data 的数据
data: function(elem, name, data) {
return internalData(elem, name, data);
},

// 移除 elem(可是DOM,可以是JS对象)上
removeData: function(elem, name) {
return internalRemoveData(elem, name);
},

// For internal use only.
// 添加或读取一个仅供内部使用的数据
_data: function(elem, name, data) {
return internalData(elem, name, data, true);
},

// 删除内部使用的数据数据
_removeData: function(elem, name) {
return internalRemoveData(elem, name, true);
},

// A method for determining if a DOM node can handle the data expando
// 检测一个属性是否可以绑定数据
// nodeType = 1 -- Element
// nodeType = 9 -- Document
acceptData: function(elem) {
// Do not set data on non-element because it will not be cleared (#8335).
//
if (elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9) {
return false;
}

// if(elem.nodeName){
// noData = jQuery.noData[elem.nodeName.toLowerCase()];
// }
var noData = elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()];

// nodes accept data unless otherwise specified; rejection can be conditional
return !noData || noData !== true && elem.getAttribute("classid") === noData;
}
});

来一个对应:

  • jQuery.expando对应Model.cacheKey
  • jQuery.cache对应Model.cache
  • jQuery.data对应Model.data

在jQuery.hasData方法内部看到了:

1
jQuery.cache[elem[jQuery.expando]]; // 取数据
1
Model.cache[indexKey][key]; // Model和jQuery原理是一样的。

有了Model这个小栗子,就能更好的理解jQuery的data模块实现原理了;当然jQuery的data模块还有很多其它的兼容性处理,防冲突处理。

HTML5的DataSet API

HTML5加入了一个关于自定义属性的API,很是方便,虽然非现代浏览器不支持,但是你可以使用jQuery,jQuery已经提供了支持。

栗子:

1
2
<!-- 自定义属性使用 data-* 开头 -->
<span data-toggle="dropdown" class="moduleTitle" data-options="234">测试</span>
1
2
3
4
5
6
7
8
9
10
11
12
// 原生操作
// 返回全部以data-开头的自定义属性.
var dataSet = document.querySelector('.moduleTitle').dataset; // DOMStringMap

// 返回options
var dataSet = document.querySelector('.moduleTitle').dataset.options;

// 新增或者修改(同步DOM元素的data-options属性)
document.querySelector('.moduleTitle').dataset.options = 234;

// 删除
delete document.querySelector('.moduleTitle').dataset.options

jQuery栗子:

1
2
<span data-toggle="dropdown" class="moduleTitle" data-options="234"
data-camel-options="234">测试</span>
1
2
3
4
5
6
7
8
9
10
11
12
// jQuery操作

// 访问所有的缓存数据,包括data-*自定义部分
var dataSet = $('.moduleTitle').data();

// 查询 (jQuery为了遵守规范自动转换data-*自定义属性为“驼峰”访问方式.)
var camelOptions = $('.moduleTitle').data('camelOptions');

// 修改 (jQuery使用自己数据缓存模块,不再同步修改data-camel-options属性)
$('.moduleTitle').data('camelOptions', 3);
$('.moduleTitle').data('camelOptions'); // 3
$('.moduleTitle').attr('data-camel-options'); // 234

注意:

​ jQuery.data访问data-自定义属性是有优先级顺序,先从$.cache中获取,如果没有,才尝试自定义属性获取。
​ jQuery.data方法只能修改自己缓存内的数据,不会修改data-
属性的数据;所以千万不要使用jQuery().attr和jQuery.data混合使用操作data-自定义属性。