jQuery源码解读 - 数据缓存系统:jQuery.data

jQuery在1.2后引入jQuery.data(数据缓存系统),主要的作用是让一组自定义的数据可以DOM元素相关联——浅显的说:就是让一个对象和一组数据一对一的关联。

一组和Element相关的数据如何关联着这个Element一直是web前端的大姨妈,而最初的jQuery事件系统照搬Dean Edwards的addEvent.js:将回调挂载在EventTarget上,这样下来,循环引用是不可忽视的问题。而在web前端中,数据和DOM的关系太过基情和紧张,于是jQuery在1.2中,正式缔造了jQuery.data,就是为了解决这段孽缘:自定义数据和DOM进行关联。

文中所说的Element主要是指数据挂载所关联的target(目标),并不局限于Element对象。

本文原创于linkFly,原文地址。

这篇文章主要分为以下知识

  • jQuery.data模型
  • jQuery.1.x中jQuery.data实现
  • jQuery.2.x中jQuery.data实现
  • 其他实现

jQuery.data模型

模型

凡存在,皆真理——任何一样事物的存在必然有其存在的理由,于我们的角度来说,这叫需求。

一组数据,如何与DOM相关联一直是web前端的痛处,因为浏览器的兼容性等因素。最初的jQuery事件系统照搬Dean Edwards的addEvent.js:将回调挂载在EventTarget上,这样下来,循环引用是不可忽视的问题,它把事件的回调都放在相应的EventTarget上,当回调中再引用EventTarget的时候,会造成循环引用。于是缔造了jQuery.data,在jQuery.event中通过jQuery.data挂载回调函数,这样解决了回调函数的循环引用,随时时间的推移,jQuery.data应用越来越广,例如后来的jQuery.queue

首先我们要搞清楚jQuery.data解决的需求,有一组和DOM相关/描述Element的数据,如何存放和挂载呢?可能有人是这样的:

使用attributes

HTML:

<div id="demo" userData="linkFly"></div>

javascript:

(function () {
            var demo = document.getElementById('demo');
            console.log(demo.getAttribute('userData'));
})();

使用HTML5的dataset

HTML:

<div id="demo2" data-user="linkFly"></div>

javascript:

        (function () {
            var demo = document.getElementById('demo2');
            console.log(demo.dataset.user);
        })();

为DOM实例进行扩展

HTML:

<div id="demo3"></div>

javascript:

        (function () {
            var demo = document.getElementById('demo3');
            demo.userData = 'demo';
            console.log(demo.userData);
        })();
虽然有解决方案,但都不是理想的解决方案,每个方案都有自己的局限性:
  • 1、只能保存字符串(或转化为字符串类型)的数据,同时曝露了数据,并且在HTML上挂载了无谓的属性,浏览器仍然会尝试解析这些属性。
  • 2、同上。
  • 3、典型的污染,虽然可以保存更强大的数据(Object/Function),但是患有代码洁癖的骚年肯定是不能忍的,更主要,如果挂载的数据中引用着这个Element,则会循环引用。
jQuery.data,则是为了解决这样的自定义数据挂载问题。

 

模型

一窥模型吧,jQuery.data在早期,为了兼容性做了很多的事情。同时,或许是因为jQuery.data最初的需求作者也觉得太过简单,所以实现的代码上让人觉得略显仓促,早期的数据仓库很是繁琐,在jQuery.2.x后,jQuery.data重写,同时终于把jQuery.data抽离出对象。

jQuery.data模型上,就是建立一个数据仓库,而每一个挂载该数据的对象,都有自己的钥匙,他和上面的代码理念并不同:

  • 上面的方案是:

    在需要挂载数据的对象上挂载数据,就好像你身上一直带着1000块钱,要用的时候直接从口袋里掏就可以了。

  • jQuery.data则是:

    建立一个仓库,所有的数据都放在这个仓库里,然后给每个需要挂载数据的对象一把钥匙,读取数据的时候拿这个钥匙到仓库里去拿,就好像所有人都把钱存在银行里,你需要的时候则拿着银行卡通过密码去取钱。

图一张:

jQuery源码解读 - 数据缓存系统:jQuery.data_第1张图片

我们暂时先不讨论数据仓库的样子,首先我们要关注数据和Element关联的关键点——钥匙,这个钥匙颇具争议,后续的几种数据缓存方式都是在对这个钥匙进行大的变动,因为这个钥匙,不得不放在Element上——即使你把所有的钱都存在银行里了,但是你身上还是要有相应的钥匙,这不得不让那些代码洁癖的童鞋面对这个问题:Element注定要被污染——jQuery.data只是尝试了最小的污染。

jQuery在创建的时候,会生成一个属性——jQuery.expando

expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" );

jQuery.expando是当前页面中引用的jQuery对象的身份标志(id),每个页面引用的jQuery.expando都是不重复且唯一的,所以这就是钥匙的关键:jQuery.expando生成的值作为钥匙,挂载在Element上,也就是为Element创建一个属性:这个属性的名称,就是jQuery.expando的值,这就是钥匙的关键。 虽然仍然要在Element上挂载自己的数据,但是jQuery尽可能做到了最小化影响用户的东西。

当然这里需要注意:通过为Element添加钥匙的时候,使用的是jQuery.expando的值作为添加的属性名,页面每个使用过jQuery.data的Element上都有jQuery.expando的值扩展的属性名,也就是说,每个使用过jQuery.data的Element都有这个扩展的属性,通过检索这个属性值来找到仓库里的数据——钥匙是这个属性值,而不是这个jQuery.expando扩展的属性名。

木图木真相:

jQuery源码解读 - 数据缓存系统:jQuery.data_第2张图片

jQuery.1.x(截至jQuery.1.11)中,内部数据和外部数据挂载在jQuery.cache不同的地方——内部数据挂载在jQuery.cache[钥匙]下,而用户数据则挂载在jQuery.cache[钥匙].data下,原因是因为内部数据如何是用户数据挂载在一起则会存在相互覆盖的情况,要把数据给隔离开。

 

jQuery.1.x中jQuery.data实现

这里的jQuery.1.x主要是指jQuery.1.11

jQuery.acceptData() - 目标过滤

因为jQuery.1.x是兼容低版本浏览器的,所以需要处理大量的浏览器兼容性,在jQuery.1.x中设计的jQuery.data是基于给目标添加属性来实现的,所以这其中找属性找钥匙找仓库很是繁琐,再加上IE低版本各种雷区,简直丧心病狂了已经。找钥匙找仓库还好说,低版本IE的雷区一踩一个爆:所以jQuery单独写了一个jQuery.acceptData用于屏蔽雷区,特别针对下面的情况:

  • applet和embed:这两个标签都是加载外部资源的,这两个标签在js里可以操作的权限简直就是缩卵了——根本行不通,所以jQuery直接给干掉了,直接让他们不能放标签。
  • flash:早期的jQuery将所有的Object标签纳入雷区,后来发现IE下的flash还是可以自定义属性的,于是针对IE的flash还是网开一面,放过了IE的flash,IE下加载flash的时候,需要对object指定一个叫做classId的属性,它的值为:clsid:D27CDB6E-AE6D-11cf-96B8-444553540000

jQuery.acceptData配合jQuery.noData做的过滤:

    jQuery.extend({
        //jQuery.cache对象,仓库
        cache: {},
        noData: {
            //有可能权限不够,所以过滤
            "applet ": true,
            "embed ": true,
            //ie的flash可以通过
            "object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
        }
        //其余API代码略
    });

    jQuery.acceptData = function (elem) {
        //确定一个对象是否允许设置Data 
        var noData = jQuery.noData[(elem.nodeName + " ").toLowerCase()],
		nodeType = +elem.nodeType || 1;
        //进行过滤
        return nodeType !== 1 && nodeType !== 9 ?
		false :
        //如果是jQuery.noData里面限定的节点的话,则返回false
        //如果节点是object,则判定是否是IE flash的classid
	!noData || noData !== true && elem.getAttribute("classid") === noData;
    };

internalData() - 挂载/读取数据

挂载和读取数据方法是同一个(下面有分步分析):首先拿到钥匙,也就是jQuery.expando扩展的属性,然后根据钥匙获取仓库,因为内部数据和用户数据都是挂载在jQuery.cache下的,所以在jQuery.cache下开辟了jQuery.cache[钥匙].data作为用户数据存放的空间,而jQuery.cache[钥匙]则存放jQuery的内部数据,将数据挂上之后,返回的结果是这个数据挂载的空间/位置,通过这个返回值可以访问到这个Element所有挂载的数据。

 function internalData(elem, name, data, pvt /* Internal Use Only */) {
        //pvt:标识是否是内部数据

        //判定对象是否可以存数据
        if (!jQuery.acceptData(elem)) {
            return;
        }
        var ret, thisCache,
              //来自jQuery随机数(每一个页面上唯一且不变的)
		    internalKey = jQuery.expando,
            /*
    		    如果是DOM元素,
		        为了避免javascript和DOM元素之间循环引用导致的浏览器(IE6/7)垃圾回收机制不起作用,
		        要把数据存储在全局缓存对象jQuery.cache中
            */
		    isNode = elem.nodeType,
		    //只有DOM节点才需要全局缓存,js对象是直接连接到对象的
            //如果是DOM,则cache连接到jQuery.cache
		    cache = isNode ? jQuery.cache : elem,
		    //如果是DOM,则获取钥匙,如果是第一次读取,则读取不到钥匙
		    id = isNode ? elem[internalKey] : elem[internalKey] && internalKey;
        //检测合法性,避免做更多的工作,pvt标识是否是内部数据
        if ((!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string") {
            return;
        }
        //没有拿到钥匙,则赋上ID
        if (!id) {
            if (isNode) {
                /*
                   DOM需要有一个全新的全局id,
                   为DOM建立一个jQuery的全局id
                   低版本代码:elem[ internalKey ] = id = ++jQuery.uuid;
                   这个deletedIds暂时忽略,当初jQuery准备重用uuid,后来被guid取代了
                */
                id = elem[internalKey] = deletedIds.pop() || jQuery.guid++;
            } else {
                //对象就不用这么麻烦了,直接挂钥匙就可以了
                id = internalKey;
            }
        }
        //从jQuery.cache中没有读取到,则开辟一个新的
        if (!cache[id]) {
            //创建一个新的cache对象,这个toJson是个空方法
            cache[id] = isNode ? {} : { toJSON: jQuery.noop };
            /*
              对于javascript对象,设置方法toJSON为空函数,
              以避免在执行JSON.stringify()时暴露缓存数据。
              如果一个对象定义了方法toJSON()
              JSON.stringify()在序列化该对象时会调用这个方法来生成该对象的JSON元素
            */
        }

            /*
              先把Object/Function的类型的数据挂上。调用方式 :
              $(Element).data({'name':'linkFly'});
              这里的判定没有调用jQuery.type()...当然在于作者的心态了...
            */
        if (typeof name === "object" || typeof name === "function") {
            if (pvt) {//如果是内部数据
                //挂到cache上
                cache[id] = jQuery.extend(cache[id], name);
            } else {
                //如果是自定义数据,则挂到data上
                cache[id].data = jQuery.extend(cache[id].data, name);
            }
        }
        //调整位置,因为有可能是取数据
        thisCache = cache[id];

        if (!pvt) {
            //如果不是内部数据(即用户自定义数据),则调整到jQuery.chche.data上
            if (!thisCache.data) {
                thisCache.data = {};
            }
           //继续调整位置
            thisCache = thisCache.data;
        }
        /*
             到了这里外面的调用方式是
             $(Element).data('name','value');
        */
        if (data !== undefined) {
            //挂上数据
            thisCache[jQuery.camelCase(name)] = data;
        }
       if (typeof name === "string") {
            ret = thisCache[name];
            if (ret == null) {
                ret = thisCache[jQuery.camelCase(name)];
            }
        } else {
           ret = thisCache;
        }

        //同时处理获取数据的情况
        return ret;
    }        

太长看起来恶心?来,我们一点点分析:

1、首先,检测是否可以存放数据,可以的话初始化操作,针对变量id要注意,这里的一直在找上面我们所说的挂载在Element上那个存放钥匙的属性,也就是jQuery.expando的值

        if (!jQuery.acceptData(elem)) {
            return;
        }
        var ret, thisCache,
             //来自就jQuery随机数(每一个页面上唯一且不变的)
		    internalKey = jQuery.expando,
            /*
        	    如果是DOM元素,
		        为了避免javascript和DOM元素之间循环引用导致的浏览器(IE6/7)垃圾回收机制不起作用,
		        要把数据存储在全局缓存对象jQuery.cache中;
		        对于javascript对象有垃圾回收机制
		        所以不会有内存泄露的问题
		        因此数据可以直接存储在javascript对象上
            */
		    isNode = elem.nodeType,
            //如果是Element,则cache连接到jQuery.cache
		    cache = isNode ? jQuery.cache : elem,
		    //如果是Element,则获取钥匙
		    id = isNode ? elem[internalKey] : elem[internalKey] && internalKey;
        //检测合法性,第一次调用$(Element).data('name')会被拦截
        if ((!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string") {
            return;
        }

2、如果没有钥匙(id),则为目标添加上钥匙,代码如下:

        //没有ID,则赋上ID
        if (!id) {
            if (isNode) {
               /*
                DOM需要有一个全新的全局id
                为DOM建立一个jQuery的全局id
                低版本代码:elem[ internalKey ] = id = ++jQuery.uuid;
                这个deletedIds暂时忽略
                id = elem[internalKey] = deletedIds.pop() || jQuery.guid++;
                */
            } else {
                //而对象不需要
                id = internalKey;
            }
        }

2、根据钥匙,尝试从cache中读仓库的位置,如果仓库中还没有这个目标的存放空间,则开辟一个,这里特别针对了JSON做了处理:当调用JSON.stringify序列化对象的时候,会调用这个对象的toJSON方法,为了保证jQuery.data里面数据的安全,所以直接重写toJSON为一个空方法(jQuery.noop),这样就不会曝露jQuery.data里面的数据。另外一种说法是针对HTML5处理的dataAttr()(下面有讲)使用JSON.parse转换Object对象,而这个JSON可能是JSON2.js引入的:JSON2.js会为一系列原生类型添加toJSON方法,导致for in循环判定是否为空对象的时候无法正确判定——所以jQuery搞了个jQuery.noop来处理这里。

        //从cache中没有读取到
        if (!cache[id]) {
            //创建一个新的cache对象,这个toJson是个空方法
            cache[id] = isNode ? {} : { toJSON: jQuery.noop };
            /*
                对于javascript对象,设置方法toJSON为空函数,
                以避免在执行JSON.stringify()时暴露缓存数据。
                如果一个对象定义了方法toJSON()
                JSON.stringify()在序列化该对象时会调用这个方法来生成该对象的JSON元素
            */
        }

3、如果是Function/Object,则直接调用jQuery.extend挂数据,把$(Element).data({'name':'linkFly'})这种调用方式的数据挂到jQuery.cache

        /*
          先把Object/Function的类型的数据挂上。调用方式 :
          $(Element).data({'name':'linkFly'});
          这里的判定没有调用jQuery.type()...当然在于作者的心态了...
        */

        if (typeof name === "object" || typeof name === "function") {
            if (pvt) {//如果是内部数据
                //挂到cache上
                cache[id] = jQuery.extend(cache[id], name);
            } else {
                //如果是自定义数据,则挂到data上
                cache[id].data = jQuery.extend(cache[id].data, name);
            }
        }

4、还有其他数据类型(String之类的)没有挂载上,这里把$(Element).data('name','value')的数据挂载上,最后要把这个data作为方法的返回值,这个返回值非常重要,从而实现也可以读取数据的功能

        //有可能是获取数据,所以开始调整位置
        thisCache = cache[id];
        //调整返回值的位置
        if (!pvt) {
            //如果不是内部数据(即用户自定义数据),则挂到jQuery.chche.data上
            if (!thisCache.data) {
                thisCache.data = {};
            }

            thisCache = thisCache.data;
        }
        /*
                如果是这样调用的:$(Element).data('name','value');
                那么刚好利用上面的thisCache(当前指向要挂载的空间)
                把数据挂上去
        */
        if (data !== undefined) {
            thisCache[jQuery.camelCase(name)] = data;
        }
        //数据全部挂好,调整返回值
        if (typeof name === "string") {
            //如果参数是一个字符串
            //则抓取这个字符串对应的数据
            ret = thisCache[name];

            //抓取失败,转换成驼峰再抓
            if (ret == null) {
            }
        } else {
            //如果参数是Object/Function,则直接返回
            ret = thisCache;
        }

        //最终返回
        return ret;
代码上非常严谨,每一步都尽可能写在最恰当的地方,这里的方法会在jQuery最外层的API中和dataAttr()(下面会讲)方法一起配合来实现挂载/读取数据。

internalRemoveData() - 移除数据

数据移除方法移除数据方便比较简单,但是当仓库中没有相应Element存储的数据的时候,会直接从仓库中删除这个存储空间(下面有分步分析):

function internalRemoveData(elem, name, pvt) {
        //移除一个data到jQuery缓存中
        if (!jQuery.acceptData(elem)) {
            return;
        }

        var thisCache, i,
		isNode = elem.nodeType,
		cache = isNode ? jQuery.cache : elem,
		id = isNode ? elem[jQuery.expando] : jQuery.expando;
        if (!cache[id]) {
            return;
        }

        if (name) {
            //获取数据
            thisCache = pvt ? cache[id] : cache[id].data;

            if (thisCache) {
                if (!jQuery.isArray(name)) {//如果并不具有数组行为
                    if (name in thisCache) {
                        //检查缓存是否有这个对象
                        name = [name];
                    } else {
                        name = jQuery.camelCase(name);
                        //转换驼峰再次尝试
                        if (name in thisCache) {
                            name = [name];
                        } else {
                            //拿不到
                            name = name.split(" ");
                        }
                    }
                } else {
                    //jQuery.map将一个类数组转转换成真正的数组
                    //注意这里使用了连接,即如果删除失败则采用驼峰命名再次尝试删除,逻辑好严谨
                    name = name.concat(jQuery.map(name, jQuery.camelCase));
                }

                i = name.length;
                //删除缓存
                while (i--) {
                    delete thisCache[name[i]];
                }

                //如果是剩下的缓存中没有数据了,则完成了任务,否则继续
                if (pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache)) {
                    return;
                }
            }
        }

        //如果不是jQuery内部使用
        if (!pvt) {
            delete cache[id].data;// data也删除
            //检测还有没有数据,还有数据则继续
            if (!isEmptyDataObject(cache[id])) {
                return;
            }
        }

        //如果是Element,则破坏缓存
        if (isNode) {
            jQuery.cleanData([elem], true);
        } else if (support.deleteExpando || cache != cache.window) {
            //不为window的情况下,或者可以浏览器检测可以删除window.属性,再次尝试删除
            //低版本ie不允许删除window的属性
            delete cache[id];
        } else {
            //否则,直接null
            cache[id] = null;
        }
    }

1、和internalData()一样,拿钥匙。

        //移除一个data到jQuery缓存中
        if (!jQuery.acceptData(elem)) {
            return;
        }

        var thisCache, i,
		    isNode = elem.nodeType,
		    cache = isNode ? jQuery.cache : elem,
                //根据jQuery标识拿钥匙
		    id = isNode ? elem[jQuery.expando] : jQuery.expando;
        //如果找不到缓存,不再继续
        if (!cache[id]) {
            return;
        }

2、找到仓库存储数据的位置,然后删除数据,这里充分的考虑了数据命名和Object参数的情况。

if (name) {
            //获取缓存的位置
            thisCache = pvt ? cache[id] : cache[id].data;
            if (thisCache) {
                if (!jQuery.isArray(name)) {//如果并不具有数组行为
                    if (name in thisCache) {
                        //检查缓存是否有这个对象
                        name = [name];
                    } else {
                        name = jQuery.camelCase(name);
                        //转换驼峰再次尝试
                        if (name in thisCache) {
                            name = [name];
                        } else {
                            /*
                              这样都还拿不到,那还是按照自己的方式拿把,
                              也就是说jQuery支持
                            $(Element).removeData('name name2 name 3')
                              这样批量删除数据,真是被jQuery宠坏了...
                            */
                            name = name.split(" ");
                        }
                    }
                } else {
                    //jQuery.map将一个类数组转转换成真正的数组
                    //注意这里使用了连接,即如果删除失败则采用驼峰命名再次尝试删除,逻辑好严谨
                    name = name.concat(jQuery.map(name, jQuery.camelCase));
                    //'name-demo name-demo2'会转换成
                    //'name-demo name-demo2 nameDemo nameDemo2'
                }

                i = name.length;
                //删除缓存
                while (i--) {
                    delete thisCache[name[i]];
                }
                /*
                 如果是剩下的缓存中没有数据了,则完成了任务,否则有不和谐的情况,要继续处理
                isEmptyDataObject专门用来检测用户缓存空间是否是空Data,
                如果缓存空间是这样的{ test:{'name':'value'} }(用户数据挂载的空间[data]是空的),就能通过
                */
                if (pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache)) {
                    return;
                }
            }
        }

3、如果数据全部删除了,那么仓库存储数据的空间也要被删除,所以接下来针对这些情况进行了处理

        //如果不是jQuery内部使用
        if (!pvt) {
            delete cache[id].data;// 连data也删除
            //检测还有没有数据,还有数据则继续
            if (!isEmptyDataObject(cache[id])) {
                return;
            }
        }

4、因为jQuery.data和jQuery.event事件系统直接挂钩,所以这里特别针对事件系统挂载的数据进行了删除处理,jQuery.cleanData方法涉及jQuery.event,所以暂不解读了。

        //如果是Element,则破坏缓存
        if (isNode) {
            //和jQuery.event挂钩,不分析了...
            jQuery.cleanData([elem], true);
        } else if (support.deleteExpando || cache != cache.window) {
            //不为window的情况下,或者可以浏览器检测可以删除window.属性,再次尝试删除
            delete cache[id];
        } else {
            //否则,直接粗暴的null
            cache[id] = null;
        }

dataAttr()和jQuery.fn.data() - 针对HTML5的dataset和曝露API

dataAttr()是特别针对HTML5的dataset进行处理的方法,用处是读取Element上HTML5的data-*属性转换到jQuery.data中,是针对HTML5的兼容,典型的老夫就是要宠死你的方法:

    function dataAttr(elem, key, data) {
        //针对HTML5做一层特别处理,等下在jQuery.fn.data中和internalData()配合使用将会大放异彩

        /*
              注意这里的一层判定,在jQuery.fn.data调用的时候
              会先调用用internalData(),然后把internalData()的返回值传递到这里,就是data
              如果data为undefined,则进行HTML5处理
        */
        if (data === undefined && elem.nodeType === 1) {
            /*
                rmultiDash = /([A-Z])/g
                针对HTML5,把驼峰命名的数据转换为连字符:
                dataSet转换为data-set
            */
            var name = "data-" + key.replace(rmultiDash, "-$1").toLowerCase();
            data = elem.getAttribute(name);//不用dataset是因为一些比较古老的手机没有被支持,楼主就被折磨过...
            if (typeof data === "string") {
                //各种丧心病狂的转换数据
                //把不同的数据类型给转换成需要的类型
                try {
                    data = data === "true" ? true :
					data === "false" ? false :
					data === "null" ? null :
                    //如果是数字
					+data + "" === data ? +data :
                    //匹配json
					rbrace.test(data) ? jQuery.parseJSON(data) :
					data;
                } catch (e) { }
                //把HTML5的数据挂到jQuery中
                jQuery.data(elem, key, data);

            } else {
                data = undefined;
            }
        }
        //返回这个data,jQuery.fn.data会调用dataAttr()并返回它的值
        return data;
    }

jQuery.data实现很简单......个屁啊,妈蛋啊看起来就是调用internalData()实现,实际上jQuery.fn.data更加的健壮,同时将各种内层的方法都联接的惟妙惟肖,当然这也意味着性能更逊色一点,

jQuery.fn.extend({
        data: function (key, value) {
            var i, name, data,
			elem = this[0],
			attrs = elem && elem.attributes;
            //$(Element).data() - 获取全部数据
            if (key === undefined) {
                //获取
                if (this.length) {
                    data = jQuery.data(elem);
                    //如果没有标志parsedAttrs的数据,则表示没有进行过HTML5的属性转换
                    if (elem.nodeType === 1 && !jQuery._data(elem, "parsedAttrs")) {
                        i = attrs.length;
                        while (i--) {
                            //那么转换HTML5的属性
                            if (attrs[i]) {
                                name = attrs[i].name;
                                if (name.indexOf("data-") === 0) {
                                    name = jQuery.camelCase(name.slice(5));
                                    //配合dataAttr进行转换
                                    dataAttr(elem, name, data[name]);
                                }
                            }
                        }
                        //放上属性parsedAttrs,表示HTML5转换完毕
                        jQuery._data(elem, "parsedAttrs", true);
                    }
                }

                return data;
            }

            //$(Element).data({ name:'linkFly',value:'hello world' });
            if (typeof key === "object") {
                //循环设置
                return this.each(function () {
                    jQuery.data(this, key);
                });
            }

            return arguments.length > 1 ?
            //$(Element).data('name','linkFly')
			this.each(function () {
			    jQuery.data(this, key, value);
			}) :
                /*
                    使用jQuery.data读取数据,如果读取不到,则调用dataAttr()读取并设置一遍HTML5的数据
                */
			elem ? dataAttr(elem, key, jQuery.data(elem, key)) : undefined;
        }
    });

1、针对获取全部数据做处理,同时在内部标识上parsedAttrs,表示这个Element已经被转换过HTML5属性了:

            if (key === undefined) {
                //获取
                if (this.length) {
                    data = jQuery.data(elem);
                    //如果没有标志parsedAttrs的数据,则表示没有进行过HTML5的属性转换
                    if (elem.nodeType === 1 && !jQuery._data(elem, "parsedAttrs")) {
                        i = attrs.length;
                        while (i--) {
                            //那么转换HTML5的属性
                            if (attrs[i]) {
                                name = attrs[i].name;
                                if (name.indexOf("data-") === 0) {
                                    name = jQuery.camelCase(name.slice(5));
                                    //配合dataAttr进行转换
                                    dataAttr(elem, name, data[name]);
                                }
                            }
                        }
                        //放上属性parsedAttrs,表示HTML5转换完毕
                        jQuery._data(elem, "parsedAttrs", true);
                    }
                }

                return data;
            }

2、如果不是读取全部数据,则情况要么是挂载数据,要么是读取数据,但在最后的一段代码比较不错,是internalData()和dataAttr()的配合使用针对HTML5 dataset的兼容:

        if (typeof key === "object") {
                //循环设置
                return this.each(function () {
                    jQuery.data(this, key);
                });
        }

        return arguments.length > 1 ?
            //$(Element).data('name','linkFly')
		this.each(function () {
			jQuery.data(this, key, value);
		}) :
                /*
                    $(Elment).data('name')  
                    这里的代码很有意思:
                    jQuery.data(elem,key)是调用internalData(),而internalData最终会返回要挂载的数据
                    如果用户挂载的数据是空的,则调用dataAttr()尝试转换HTML5的数据返回并且给挂到jQuery.data
                */
        elem ? dataAttr(elem, key, jQuery.data(elem, key)) : undefined;
}

这里重点照顾最后一句,它实现了:

  • 读取数据:$(Element).data('demo');
  • 如果读取不到,读取HTML5的dataset数据并挂载到jQuery.cache中。

如果到了这里,那么调用方式会是:$(Elment).data('name'),这时候的处理方法就是:

  • jQuery.data底层是internalData(),当第三个参数为空的时候,则是读取数据
  • internalData()如果读取不到数据,则调用dataAttr(),而dataAttr第三个参数为undefined的时候,则会读取HTML5的dataset,然后再调用jQuery.data()(注意不是jQuery.fn.data)再挂一次数据。
好的各位爷,至此 jQuery.1.x代码已经读完了,要不您老喝点茶看看窗外放松一下消化一下上面的代码?:
  • jQuery.expando是钥匙的关键,将jQuery.expando的值挂在Element上,就好像在你身上挂了一张银行卡,而银行卡的密码,则是jQuery.guid(累加不重复)。
  • 通过钥匙找到仓库,进行操作。
  • internalData()的思路很值得借鉴,在挂数据的时候同时取数据,尤其在jQuery.cache这个相对比较复杂的环境里,如何更高效的取数据本身就是一件值得思考的事情。
  • internalRemoveData()实现了深度删除数据,尽可能让数据仿佛从未存在过,并且尝试了多种删除。
  • dataAttr()是针对HTML5特别的兼容处理。
  • internalData()方法非常的严谨,但是它仍然只是为了挂载数据和移除数据而生,非常纯粹而简单的工作着,真正让jQuery健壮的是jQuery.fn.data。

jQuery.2.x中jQuery.data实现

这里的jQuery.2.x主要是指jQuery.2.1

jQuery.2.x中,jQuery.data终于决定被好好深造一下了,过去1.x的代码说多了都是泪,jQuery.2.x没有了兼容性的后顾之忧,改写后的代码读起来简直不要太舒适啊。

jQuery.2.x中,为数据缓存建立了Data对象,一个Data对象表示一个数据仓库——用户数据和内部数据各自使用不同的Data对象,这样就不需要在仓库里翻来翻去的查找数据存储的位置了(jQuery.cache[钥匙]和jQuery.cache[钥匙].data),思路上,仍然和jQuery.1.x一致,采用扩展属性的方式实现,关键点在Data.prorotype.key()上。

Data对象 - 数据仓库

Data对象经过封装以后衍生了这些API:

  • key:专门用来获取和放置Element的钥匙。
  • set/get:放置和获取数据
  • access:通用API,根据参数既然可以放置也可以获取数据
  • remove:移除数据
  • hasData:检测是否有数据
  • discard:丢弃掉这个Element的存储空间

jQuery源码解读 - 数据缓存系统:jQuery.data_第3张图片

其他的实现都比较简单,我们需要关注钥匙这里,也就是Data.prototype.key()

Data.prototype.key() - 钥匙

Data.prototype = {
        //获取缓存的钥匙
        key: function (owner) {
            //检测是否可以存放钥匙
            if (!Data.accepts(owner)) {
                return 0; //return false
            }

            var descriptor = {},
                //获取钥匙,还是在Element上挂载jQuery属性
	         unlock = owner[this.expando];
            //如果钥匙没有则创建
            if (!unlock) {
                unlock = Data.uid++;
                try {
                    //把expando转移到Data中,没一个Data实例都有不同的expando
                    descriptor[this.expando] = { value: unlock };
                    //参考:http://msdn.microsoft.com/zh-cn/library/ie/ff800817%28v=vs.94%29.aspx
                    //这个属性不会被枚举
                    Object.defineProperties(owner, descriptor);
                } catch (e) {
                    //如果没有Object.defineProperties,则采用jQuery.extend
                    descriptor[this.expando] = unlock;
                    jQuery.extend(owner, descriptor);
                }
            }
            if (!this.cache[unlock]) {
                this.cache[unlock] = {};
            }
            //返回这个钥匙
            return unlock;
        }
    };

因为用户数据和jQuery内部数据通过Data分离,所以set/get在拿到钥匙之后都比较简单。

access() - 通用接口

在创建Data对象的时候,顺便为jQuery创建了静态方法——jQuery.access:通用的底层方法,既能设置也能读取,它应用在jQuery很多API中,例如:Text()、HTML()等。

var access = jQuery.access = function (elems, fn, key, value, chainable, emptyGet, raw) {
        //元素,委托的方法,属性名,属性值,是否链式,当返回空数据的时候采用的默认值,fn参数是否是Function
        //一组通用(内部)方法,既然设置也能获取Data
        var i = 0,
		len = elems.length,
		bulk = key == null;
        //Object
        if (jQuery.type(key) === "object") {
            //如果是放数据,Object类型,则循环执行fn
            chainable = true;//这里修正了是否链式....
            for (i in key) {
                jQuery.access(elems, fn, i, key[i], true, emptyGet, raw);
            }

            // Sets one value
        } else if (value !== undefined) {
            chainable = true;
            //如果设置的value是Function
            if (!jQuery.isFunction(value)) {
                raw = true;
            }
            //当参数是这样的:access(elems,fn,null)
            if (bulk) {
                if (raw) {
                    //参数是这样的:access(elems,fn,null,function)
                    fn.call(elems, value);
                    fn = null;
                } else {
                    //参数是这样的:access(elems,fn,null,String/Object)
                    bulk = fn;//这里把fn给调换了
                    fn = function (elem, key, value) {
                        //这个jQuery()封装的真是....
                        return bulk.call(jQuery(elem), value);
                    };
                }
            }
            //到了这里如果还可以执行的话那么参数是:access(elems,fn,key,Function||Object/String)
            if (fn) {
                for (; i < len; i++) {
                    //循环每一项执行
                    fn(elems[i], key, raw ? value : value.call(elems[i], i, fn(elems[i], key)));
                }
            }
        }

        return chainable ?
            //如果是设置数据,这个elems最终被返回,而在jQuery.fn.data中这个elems是this——也就是jQuery对象,保证了链式
            elems :

        // Gets
            //如果上面的设置方法都没有走,那么就是获取
		bulk ?//bulk是不同的工作模式,参阅jQuery.css,jQuery.attr
			fn.call(elems) :
			len ? fn(elems[0], key) : emptyGet;
    };

jQuery.fn.data() - 曝露API

相比jQuery.1.x代码更加的细腻了许多,这里配合着上面定义的access()使用,为每一个循环的jQuery项设置和读取数据,阅读起来比较轻松。

jQuery.fn.extend({
        data: function (key, value) {
            var i, name, data,
			elem = this[0],
			attrs = elem && elem.attributes;

            // 获取全部的数据,和1.x思路一致
            if (key === undefined) {
                if (this.length) {
                    data = data_user.get(elem);

                    if (elem.nodeType === 1 && !data_priv.get(elem, "hasDataAttrs")) {
                        i = attrs.length;
                        while (i--) {

                            // Support: IE11+
                            // The attrs elements can be null (#14894)
                            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;
            }

            // 设置Object类型的的数据
            if (typeof key === "object") {
                return this.each(function () {
                    data_user.set(this, key);
                });
            }
            //调用jQuery.access
            return access(this, function (value) {
                //value则是挂载的数据名(即使外面挂载的Object也会被拆开到这里一个个循环执行)
                var data,
		     camelKey = jQuery.camelCase(key);//转换驼峰
                if (elem && value === undefined) {
                    //拿数据
                    data = data_user.get(elem, key);
                    if (data !== undefined) {
                        return data;
                    }
                    //用驼峰拿
                    data = data_user.get(elem, camelKey);
                    if (data !== undefined) {
                        return data;
                    }
                    //用HTML5拿
                    data = dataAttr(elem, camelKey, undefined);
                    if (data !== undefined) {
                        return data;
                    }
                    return;
                }
                //循环每一项设置
                this.each(function () {
                    //提前设置驼峰的...
                    data_user.set(this, camelKey, value);
                    if (key.indexOf("-") !== -1 && data !== undefined) {
                    //如果有name-name命名再设一边
                        data_user.set(this, key, value);
                    }
                });
            }, null, value, arguments.length > 1, null, true);
        },

        removeData: function (key) {
            return this.each(function () {
                //调用相应Data实例方法移除即可
                data_user.remove(this, key);
            });
        }
    });
各位看官到了这里可以继续小憩一下,后面我们再来谈谈关于这个jQuery.data更多有意思的事情...总结一下, jQuery.2.x的缓存设计理念清晰,最主要的就是封装成了Data对象以后将用户数据和jQuery内部使用的数据隔离开,这是最大的改进。移动端的 Zepto里的Zepto.data是 jQuery.data.2.x的浓缩版。

其他实现

这些实现都是在司徒正美的《javascript框架设计》 - "数据缓存系统"一章里读到的,有必要宣传和感谢一下这本书,了解了很多代码的由来促进了理解。

这些实现其实都是针对钥匙怎么交给Element这个问题上进行的探索。

valueOf()重写

jQuery.2.x最初设计的jQuery.data中,作者也在为Element挂载这个expando属性作为钥匙而头疼,于是给出了另外一种钥匙的挂载方法——重写valueOf()。 Waldron

在为Element挂载钥匙的时候,不再给这个Element声明属性,而是通过重写Element的valueOf方法实现。

虽然我翻了jQuery.2.0.0 - jQuery.2.1.1都没有找到这种做法,但觉得还是有必要提一下:

    function Data() {
            this.cache = {};
        };
        Data.uid = 1;
        Data.prototype = {
            locker: function (owner) {
                var ovalueOf,
                    unlock = owner.valueOf(Data);
                /*
                owner为元素节点、文档对象、window
                传递Data类,如果返回object说明没有被重写,返回string则表示已被重写
                整个过程被jQuery称之为开锁,通过valueOf得到钥匙,进入仓库
                */
                if (typeof unlock !== 'string') {
                    //通过闭包保存,也意味着内存消耗更大
                    unlock = jQuery.expando + Data.uid++;
                    //缓存原valueOf方法
                    ovalueOf = owner.valueOf;
                    Object.defineProperty(owner, 'valueOf', {
                        value: function (pick) {
                            //传入Data
                            if (pick === Data)
                                return unlock; //返回钥匙
                            return ovalueOf.apply(owner); //返回原valueOf方法
                        }
                    });
                }
                if (!this.cache[unlock])
                    this.cache[unlock] = {};
                return unlock;
            },
            get: function (owner, key) {
                var cache = this.cache[this.locker(owner)];
                return key === undefined ? cache : cache[key];
            },
            set: function (owner, key, value) {
                //略
            }
            /*其他方法略*/
        };

思路上很是新颖——因为在js中几乎所有的js数据类型(null,undefined除外)都拥有valueOf/toString方法,所以直接重写Element的valueOf,在传入Data对象的时候,返回钥匙,否则返回原valueOf方法——优点是钥匙隐性挂到了Element上,保证了Element的干净和无需再考虑挂属性兼不兼容等问题了,而缺点就是采用闭包,所以内存消耗更大,或许jQuery也觉得这种做法的内存消耗不能忍,所以仍未采用——相比较放置钥匙到Element的方式,还是后者更加的纯粹和稳定。

Array.prototype.indexOf()

Array.prototype.indexOf()是ECMAScript 5(低版本浏览器可以使用代码模拟)定义的方法——可以从一组Array中检索某项是否存在?存在返回该项索引:不存在则返回-1。听起来很相似?没错,它就是String.prototype.indexOf()的数组版。

正是因为提供了针对数组项的查找,所以可以采用新的思路:

  • 1、将使用data()方法挂载数据的Element通过闭包缓存到一个数组中
  • 2、当下次需要检索和这个Element关联的数据的时候,只需要通过Array.ptototype.indexOf在闭包中查找到这个数组即可,而闭包中这个数组查找到的索引,就是钥匙。

代码如下:

        (function () {
            var caches = [],
                add = function (owner) {
                    /*

                    //拆开来是这样子的
                    var length = caches.push(owner);//返回Array的length
                    return caches[length - 1] = {};//新建对象并返回
                    */
                    return caches(caches.push(owner) - 1) = {};
                },
            addData = function (owner, name, data) {
                var index = caches.indexOf(owner), //查找索引,索引即是钥匙
                //获取仓库
                    cache = index === -1 ? add(owner) : caches[index];
                //针对仓库放数据即可
            }
            //其他代码略
        })();

这样就不需要在Element上挂载自定义的属性(钥匙)了——然而因为每个使用过data()的Element都会在缓存下来,那么内存的消耗必不可免,相比上一种重写valueOf重写消耗更加的不能直视,这是一个有趣但并不推荐的解决方案。

WeakMap

技术总是层出不穷的,对于目前的我们来说可望不可及的ECMAScript 6定义了新的对象——WeakMap,请参考这三篇:

  • [译]ECMAScript 6中的集合类型,第三部分:WeakMap
  • WeakMap - MDN
  • WeakMap 对象 (JavaScript) - MSDN

WeakMap对象的键值持有其所引用对象的弱引用——当那个对象被垃圾回收销毁的时候,WeakMap对象相应的键值也会被删除。它使用get/set方法将成员添加到WeakMap——简直就是为数据缓存系统/jQuery.data量身定做的。使用它,我们Data.key()方法可以改写成下面的代码:

    (function () {
            var caches = new WeakMap(),//缓存中心
                addData = function (owner, name, data) {
                    //根据Element获取相应仓库存储的空间
                    var cache = caches.get(owner);
                    //如果获取不到,则开辟空间
                    if (!cache) {
                        cache = {};
                        //放到WeakMap对象中
                        caches.set(owner, cache);
                    }
                    //挂数据
                    cache[name] = data;
                    return cache;
                },
                removeData = function (owner, name) {
                    var cache = caches.get(owner);
                    //name为undefined的时候返回全部data,否则返回name指定的data
                    return name === undefined ? cache : cache && cache[name];
                }
            //其他代码略
        });

引用

  • 手抄源码(有注释):jQuery.data.js
  • 参考书籍:司徒正美 - 《javascript框架设计》第8章:数据缓存系统
作者:linkFly
原文: http://www.cnblogs.com/silin6/p/jQuery_data.html
出处: www.cnblogs.com/silin6/
声明:嘿!你都拷走上面那么一大段了,我觉得你应该也不介意顺便拷走这一小段,希望你能够在每一次的引用中都保留这一段声明,尊重作者的辛勤劳动成果,本文与博客园共享。

如果你觉得这篇文章不错,请随手点一下右下角的“推荐”,举手之劳,却鼓舞人心,何乐而不为呢?

你可能感兴趣的:(jQuery源码解读 - 数据缓存系统:jQuery.data)