study icon indicating copy to clipboard operation
study copied to clipboard

jQuery的数据缓存系统

Open 24wangchen opened this issue 10 years ago • 1 comments

数据缓存系统最早在jQuery1.2版本开始引入,它是用于关联操作对象和与之相关的数据的一种机制。在DOM中,我们通常操作的数据有3种:元素节点、文档对象与window对象。1.2之前的jQuery事件系统照搬Dean Edwards的addEvent.js,然而addEvent在实现有个缺憾,它将事件的回调都放在了EventTarget之上,这会引起循环引用,IE下就会有内存泄露,如果EventTarget是window对象,又会引发全局污染。

数据缓存系统,除了规避以上这两个风险外,我们还可以有效地保存不同方法产生的中间变量,而这些变量会对另一个模块的方法有用,解耦方法间的依赖。对于jQuery来说,它的事件克隆乃至后来的队列实现都离不开缓存系统。

数据缓存产生的需求无外乎就是描述一个DOM节点的相关数据,以下有几种方案:

使用attributes
<div id="demo" node-type="test"></div>
var demo = document.getElementById('demo');
console.log(demo.getAttribute('node-type'));
使用html5的dataset
<div id="demo" data-type="test"></div>
var demo = document.getElementById('demo');
console.log(demo.dataset.type);
DOM扩展
<div id="demo"></div>
var demo = document.getElementById('demo');
demo['node-type'] = 'test';
console.log(demo['node-type']);

以上解决方案虽然解决了问题,但存在局限性:

  • 方案1、2只能保存字符串的数据,同时曝露了数据,并且在DOM上挂载了无谓的属性,浏览器仍然会尝试解析这些属性
  • 方案3污染DOM,同时存在循环引用的风险

jQuery的数据缓存系统解决了上述问题 jQuery建立一个仓库,所有的数据放置于此,需要挂载数据的对象分配一个key,通过这个key到这个仓库中获取值

实现

jQuery在创建的时候,生成一个属性expando,每个需要挂载的对象中存在一个属性,即expando的值,该属性的值+随机数就是仓库中所对应的key

jQuery.extend({
    expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" )
})
//jQuery + (版本号 + 随机数)_去除非数字

Data对象

jQuery中的data有两种,一种是用户可以操作的data_user,另一种是jQuery事件系统之类的内部实现使用的私有数据data_priv,但他们都是同一个Data原型函数实例化出来的。

初始化
function Data(){
    //初始化时,将该对象的cache置为{0:{}}
    Object.defineProperty( this.cache = {}, 0, {
        get: function(){
            return {};
        }
    });
    //jQuery2110499953880906105040.42317264270968735
    this.expando = jQuery.expando + Math.random();
}
//初始化Data的唯一标示
Data.uid = 1;
//Data可以接受的数据类型
Data.accepts = jQuery.acceptData;
Data可以接受的数据类型
jQuery.acceptData = function( owner ){
    //element、document、object
    return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );
}
Data原型
  • 获取key
  • get/set
  • access 根据参数选择get或者set
  • remove 移除数据
  • hasData 检查是否有数据
  • discard 删除该储存空间

1、key

  • this.expando每次实例化生成唯一
  • unlock为Data.uid自增
  • this.expando为key,unlock为value,添加到owner中;unlock作为key在data_priv/data_user中找到对应的value
Data.prototype.key = function( owner ){
    //非Data允许的对象直接返回0
    if( !Data.accepts( owner ) ){
        return 0;
    }

    var descriptor = {},
    unlock = owner[ this.expando ];
    //是否已有cache
    if( !unlock ){
        unlock = Data.uid++;

        try{
            //防止此属性被遍历和改写
            descriptor[ this.expando ] = { value: unlock };
            Object.defineProperties( owner, descriptor );
        }catch (e) {
            descriptor[ this.expando ] = unlock;
            jQuery.extend( owner, descriptor );
        }
    }
    //初始化cache
    if( !this.cache[ unlock ] ){
        this.cache[ unlock ] = {};
    }
    return unlock;
}

2、set

  • 通过自身的key方法找到owner中的key值,进而获取到cache,对cache进行赋值操作
Data.prototype.set = function( owner, data, value ){
    var prop,
          unlock = this.key( owner ),
          cache = this.cache[ unlock ];
    //data为字符串
    if( typeof data === 'string' ){
        cache[ data ] = value;
    }else{
        //cache没有可遍历的值
        if( jQuery.isEmptyObject( cache ) ){
            jQuery.extend( this.cache[ unlock ], data );
        }else{
            //此处采用的是浅复制
            for( prop in data ){
                cache[ prop ] = data[ prop ];
            }
        }
    }
    return cache;
}

3、get

  • 如果没有key,就获取所有cache,否则获取对应的cache[key]
Data.prototype.get = function( owner, key ){
    var cache = this.cache[ this.key( owner ) ];
    return key === undefined ? cache : cache[ key ];
}

4、access

  • access( owner )或者access( owner, key )->get方法,否则->set方法
Data.prototype.access = function( owner, key, value ){
    var stored;
    if( key === undefined || ( ( key && typeof key === 'string') && value === undefined ) ){
        stored = this.get( owner, key );
        return stored === undefined ? stored : this.get( owner, jQuery.camelCase( key ) );
    }
    this.set( owner, key, value );

    return value !== undefined ? value : key;
}

5、remove

  • 如果key为空,此时将cache置为空对象
  • key为数组时,将数组值用jQuery.camelCase方法执行一遍追加到原数组中,以此来兼容带有'-'的情况;key为字符串时直接使用该方法
  • 最后遍历执行delete操作
  • camelCase将'-xxx'全部转化为'XXX'
Data.prototype.remove = function( owner, key ){
    var i, name, camel,
          unlock = this.key( owner ),
          cache = this.cache[ unlock ];
    if( key === undefined ){
        this.cache[ unlock ] = {};
    } else {
        if( jQuery.isArray( key ) ){
            name = key.concat( key.map( jQuery.camelCase ) );
        } else {
            camel = jQuery.camelCase( key );
            if( key in cache ){
                name = [ key, camel ];
            } else {
                name = camel;
                //此处兼容了空格分隔key的情况
                name = name in cache ? [ name ] : ( name.match( rnotwhite ) || [] );
            }
            i = name.length;
            while( i-- ){
                delete cache[ name[ i ] ];
            }
        }
    }
}

6、hasData

  • 使用jQuery.isEmptyObject方法判断,即是否有可枚举的属性
Data.prototype.hasData = function( owner ){
    return !jQuery.isEmptyObject( this.cache[ owner[ this.expando ] ] || {});
}

7、discard

  • 清空该owner的储存空间
Data.prototype.discard = function( owner ){
    if( owner[ this.expando ] ){
        delete this.cache[ owner[ this.expando ] ];
    }
}
$('').data()与$.data区别

看下面代码

<div id="test"></div>
var node1 = $('#test');
var node2 = $('#test');

//===========$('').data()============
node1.data('a',1);
node2.data('a',2);

node1.data('a');//2
node2.data('a');//2

//===========$.data()==============
$.data(node1,'b',1);
$.data(node2,'b',2);

$.data(node1,'b');//1
$.data(node2,'b');//2

$('').data()会覆盖,$.data()则不会。分析下源代码

jQuery.extend({
    data: function( elem, name, data ){
        //直接调用Data.prototype.access,传入的是整个elem的jQuery对象
        return data_user.access( elem, name, data );
    }
})

jQuery.fn.extend({
    data: function( key,value ){
        return access( this, function( value ){
            //此处是遍历dom每个节点,将缓存数据与dom节点关联
            this.each(function(){
            })
        })
    }
})
  • $.data()针对的是每一个elem的jQuery对象,由于node1和node2是不同的jQuery对象,所以自然有不同的{key:value}缓存数据
  • $('').data()针对的是具体的dom节点,node1和node2都是指向#container这个dom,所以引用的是同一个{key:value}缓存数据

jQuery自身扩展

这里都是调用的Data原型上的方法

jQuery.extend({
    hasData: function( elem ){
        return data_user.hasData( elem ) || data_priv.hasData( elem );
    },
    data: function( elem, name, data ){
        return data_user.access( elem, name, data );
    },
    removeData: function( elem, name ){
        return data_user.remove( elem, name );
    },
    _data: function( elem, name, data ){
        return data_priv.access( elem, name, data );
    },
    _removeData: function( elem, name ){
        data_priv.remove( elem, name );
    }
})

jQuery对象API扩展

$('').data()

  • 获取data全部值时,首先获取data_user中的数据,然后将html5中的data-属性的值添加至data_user中,其中将data_priv中的hasDataAttrs作为标志位,如果为true,则不再遍历其data-属性
jQuery.fn.extend({
    data: function(key, value){
        var i, name, data,
              elem = this[0],
              attrs = elem && elem.attributes;

        if( key === undefined ){
            if( this.length ){
                data = data_user.get( elem );
                //将elem节点中的data-xxx 属性放入data_user
                if( elem.nodeType === 1 &&  !data_priv.get( elem, "hasDataAttrs" ) ){
                    i = attrs.length;
                    while( i-- ){
                        if( attrs[ i ] ){
                            name = attrs[i].name;
                            if( name.indexOf( "data-" ) === 0 ){
                                name = jQuery.camelCase( name.slice(5) );
                                dataAttr( elem, name, data[ name ] )
                            }
                        }
                    }
                    data_priv.set( elem, "hasDataAttrs", true );
                }
            }
            return data;
        }
        //值为对象时
        if( typeof key === "object" ){
            return this.each(function(){
                data_user.set( this, key );
            })
        }

        return access( this, function( value ){
            //access在jQuery中是一个可以set/get的多功能函数,此处会使用this.each遍历jQuery对象,set/get数据
        }, null, value, arguments.length > 1, null, true );
    }
})

dataAttr函数

  • 将elem节点中的data-xxx属性值放入data_user中,提升效率
var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,
      rmultiDash = /([A-Z])/g;
function dataAttr( elem, key, data ){
    if( data === undefined && elem.dataType === 1 ){
        name = "data-" key.replace( rmultiDash, "-$1" ).toLowerCase();
        data = elem.getAttribute(name);

        if(typeof data === "string"){
            try{
                data = data === "true" ? true ://转化true
                    data === "false" ? false ://转化false
                    data === "null" ? null ://转化null
                    +data + "" === data ? +data ://转化数字
                    rbrace.test( data ) ? jQuery.parseJSON( data ) ://转化json字符串
                    data;
            } catch ( e ) {}
            data_user.set( elem, key, data );
        }else{
            data = undefined;
        }
    }
    return data;
}

24wangchen avatar Apr 19 '15 14:04 24wangchen

mark

codezyc avatar Apr 24 '15 02:04 codezyc