lxjwlt's blog

jQuery.data原理介绍

面试官说:你知道jQuery库的原理吗?

我说:最近我写了个类似jQuery的库,主要实现的是事件绑定那一块的代码

面试官:好,那你说说animate的原理

我:……

首先我们来回顾下jQuery.data的使用示例:

// 存储 某键的数据
$.data(elem, 'keyName', 'data value');

// 获取 某键的数据
$.data(elem, 'keyName')

// 获取 所有数据
$.data(elem)

简单实现

存储空间

为了存储数据,需要开辟一个存储空间:

jQuery.cache = {};

jQuery.cache中,jQuery会为每个DOM元素单独开辟一个的存储空间,问题是:如何区分每个DOM元素从而找到他们的数据呢?

标识符

jQuery给DOM元素分配一个标识符,DOM元素可以通过这个标识符在jQuery.cache中找到自身的数据。以下是jQuery的标识符生成器:

// 通过不断的 自加 来生成唯一的标识符
// 类似: 标识符 = jQuery.guid++;
jQuery.guid = 1;

现在有了标识符,但是标识符该如何存放呢?

首先,标识符应该以键值的形式存放在对应的DOM元素上,为了避免键名重复,jQuery随机生成了一个键名来存放标识符:

// 产生的键名 类似于: "jQuery044958585570566356"
jQuery.expando = 'jQuery' + (Math.random() + '').replace(/\D/g, '');

于是,用这个随机生成的键名来存放标识符:

elem[jQuery.expando] = jQuery.guid++;

这样,DOM元素就有了独一无二的标识符了,下面我们可以根据标识符存取数据。

存取数据

$.data方法实际上调用的是jQuery库内部的internalData函数:

jQuery.data = function(elem, key, data) {
     return internalData(elem, key, data);
};

所以实际上,数据存取的操作是在internalData函数内实现的,大致如下:

function internalData(elem, key, data) {
    var thisCache,
        internalKey = jQuery.expando,
        id = elem[internalKey];

    // 如果标识符不存在,为elem创建标识符
    if (!id) id = elem[internalKey] = guid++;

    // 如果elem没有存储空间,为elem创建存储空间
    if (!jQuery.cache[id]) jQuery.cache[id] = {};

    // 获取elem的存储空间
    thisCache = cache[id];

    // 存储数据
    if (data !== undefined) thisCache[key] = data;

    // 判断是否指定了键名:
    // - 如果指定了键名,返回对应的数据
    // - 否则,返回所有数据
    return typeof key === 'string' ? thisCache[key] : thisCache;
}

jQuery.data的基本原理如上,但是……

jQuery没这么简单

jQuery作为一个库,要考虑的细节非常的多,所以代码远远比上述代码复杂,下面介绍一下jQuery.data另外一些特性。

内部数据和用户数据

jQuery库会使用jQuery.data方法存储一些内部使用的数据,比如queue队列,on事件绑定等等,这些方法都需要存储空间来存储数据。

为了区分内部使用的数据和用户定义的数据,jQuery将内部使用的数据直接存储在cache[id]里面,而用户定义的数据则存储在cache[id].data中,形如:

// 标识符
var id = elem[jQuery.expando];

// jQuery.cache[id]形如:
{
    // 存储用户数据
    data: {
        name: 'lxjwlt',
        age: '22'
    },

    // 这些都是内部使用的数据
    events: {},
    handle: function() {},
    fxqueue: ['inprogress']
}

检查是否支持支持数据存储

在进行数据存储之前,jQuery库会先检查传进来的对象是否支持数据存储。

支持data方法的有:js普通对象、根节点和大部分元素节点(其中applet、embed和object元素是不支持设置expando属性的,所以不支持data方法)。代码大致如下:

jQuery.noData = {
    'applet': true,
    'embed': true,
    'object': true
};
jQuery.acceptData = function(elem) {
    var noData = jQuery.noData[elem.nodeName.toLowerCase()],

        // 使用‘或’是为了避免对 js普通对象 的误判
        nodeType = +elem.nodeType || 1;

    return nodeType !== 1 && nodeType !== 9 ? false : !noData;
};

js对象的数据存储

其实普通的js对象(plain object)也是可以进行数据存储,但是不同于DOM对象的数据存储方式,js对象的数据是直接存储在该对象本身之中,代码如下:

function func(obj, key, data) {
    obj[jQuery.expando][key] = data;
    return typeof key === 'string' ? obj[jQuery.expando][key] : obj[jQuery.expando];
}

// 存储过数据的对象 形如:
{
    // 用户定义的数据
    "jQuery044958585570566356": { //... },

    // 对象本身的属性
    prop: 'prop1 value',
    otherProp: 'other value'

}

获取HTML5的data数据

当jQuery.data方法在jQuery.cache中没有找到数据时,jQuery.data会在DOM元素的data-*属性中查找数据。

我们先回顾一下HTML5中data的用法:

<!-- 
    键的命名要用 横线 分隔 
    键名中大写字母都会被转换成小写字母,所以驼峰法的命名无效
-->
<div data-node-name="div element">...</div>

在jQuery中,dataAtrr函数用于获取html5的data数据,代码大致如下:

function dataAtrr(elem, key) {
    var data, name;
    if (elem.nodeType !== 1) return;

    // 键名转换,如: theNodeName  ->  the-node-name
    name = 'data-' + key.replace(/([A-Z])/g, '-$1');

    data = elem.getAttribute(name);

    // 如果没有设定数据,获取的data为null,要将其改写为undefined
    if (data === null) data = undefined;

    return data;
}

移除数据

jQuery提供了删除用户定义数据的方法 jQuery.removeData:

jQuery.removeData(elem, key);

jQuery还定义了删除所有数据的方法: jQuery.cleanData(只在jQuery内部使用),该方法的使用的场景有:如果某个元素节点被删除,那么它存储的所有数据都没有存在的必要了,所以要清空该元素节点的所有数据。例如在jQuery.fn.remove方法中就使用了jQuery.cleanData方法。

值得一提的是,节点上的标识符可以被循环利用,所以当清空了节点的存储空间,jQuery会回收标识符,以供下一个节点使用:

// 回收的标识符存储在deleteIds中
deletedIds.push( id );

所以,那句老话还是有道理的:

既然选择使用jQuery库,那么就尽可能地使用jQuery来实现代码功能