jQuery源码解析(1)—— jq基础、data缓存系统

来源:互联网 发布:端口检测工具 编辑:程序博客网 时间:2024/06/05 17:11

闲话

jquery 的源码已经到了1.12.0 版本,据官网说1版本和2版本若无意外将不再更新,3版本将做一个架构上大的调整。但估计能兼容IE6-8的,也许这已经是最后的样子了。

我学习jq的时间很短,应该在1月,那时的版本还是1.11.3,通过看妙味课堂的公开课视频和文档里的所有api的注解学习。

源码则是最近些日子直接生啃,跳过了sizzle和文档处理的部分(待业狗压力大,工作以后再看),关注datareadyeventqueueDefferred(jq的promise编程)、ajaxanimation的处理,初看甚至有点恶心,耐着性子坚持却叹其精妙,在这里记录下来加深印象。

(本文采用 1.12.0 版本进行讲解,用 #number 来标注行号)

jQuery初始化

整体封装上,考虑了两点(写轮子的重要参考)

模块化支持:通过 noGlobal 变量来决定是否绑定到全局变量,返回值为jQuery
冲突避免:保存window.$ window.jQuery,可以调用noConflict 还原,返回jQuery对象。(noGlobal为true时不需要)

主体使用了如下结构,抽离 if 分支,只关注主体逻辑的书写

(function( a, fun ) {    // 判断是否调用、如何调用fun})(a, function( _a, _b ) {    // 具体逻辑});/* ---- 区别于如下模式 ----*/(function( _a, _b) {    if (判断A) {        // 调整参数或退出    } else if (判断B) {        // 调整参数或退出    } ...    // 这里开始时具体逻辑})( a );

[源码]

(function( global, factory ) {    if ( typeof module === "object" && typeof module.exports === "object" ) {        module.exports = global.document ?            factory( global, true ) :            // 若无window,则模块存为函数,可取出通过传入window来调用一次            // noGlobal为false,一定会污染全局            function( w ) {                if ( !w.document ) {                    throw new Error( "jQuery requires a window with a document" );                }                return factory( w );            };    } else {        factory( global );    }})(typeof window !== "undefined" ? window : this, function( window, noGlobal ) {    /* ---- jQuery具体逻辑 ----*/    ...    // 对AMD的支持,#10991    if ( typeof define === "function" && define.amd ) {        define( "jquery", [], function() {            return jQuery;        } );    }    // 保存之前的 window.$   window.jQuery    var _jQuery = window.jQuery,        _$ = window.$;    jQuery.noConflict = function() {        if ( window.$ === jQuery ) {            window.$ = _$;        }        if ( deep && window.jQuery === jQuery ) {            window.jQuery = _jQuery;        }        return jQuery;    };    // 模块化时,不设置全局    if ( !noGlobal ) {        window.jQuery = window.$ = jQuery;    }    return jQuery;});


jQuery工厂结构

jq为了能有$.func、 $().func两种调用方法,选择了共享 jQuery.prototypereturn new jQuery.fn.init

这并不是唯一的方式(可以如下),之所以选择如此,个人认为应该是使用频率太高,这样每次可以省掉两次类型判断。而 jQuery.fn 我想也是起到简写、别名的作用

jQuery.extend/fn.extend 则决定了jq重要的插件扩展机制

var jQuery = function( selector, context ) {    if ( this instanceof jQuery) {        return new jQuery( selector, context );         }    // 具体逻辑}

[源码]

// #71var jQuery = function( selector, context ) {    return new jQuery.fn.init( selector, context );};// #91, 原型的别名 fn,省字符jQuery.fn = jQuery.prototype = {    ...};// #175, jQuery 扩展方法extend定义,只填一个参数表示对this进行扩展jQuery.extend = jQuery.fn.extend = function(){    ...};// #2866, 支持选择器,节点html,element,jq对象,函数var init = jQuery.init = function( selector, context, root ) {    ...};// #2982, 跟jQuery共享原型对象init.prototype = jQuery.fn;


jQuery链式调用

精髓:通过 return this , return this.pushStack() , return this.prevObject 实现链式调用、增栈、回溯

[源码]

// # 122, 创建一层新的堆栈, 并引用 prevObjectpushStack: function( elems ) {    // merge -> #433, 支持把数组、类数组的0-length项添加到第一个参数    var ret = jQuery.merge( this.constructor(), elems );    ret.prevObject = this;    ret.context = this.context;    return ret;};// #164, 可以回溯上一级 preObjectend: function() {    return this.preObject || this.constructor();}// #3063, 添加到新层 this.constructor()add: function( selector, context ) {    return this.pushStack(        // 去重排序        jQuery.uniqueSort(            // 合并到 this.get()            jQuery.merge( this.get(), jQuery( selector, context ) )        )    );},addBack: function( selector ) {    // add将把结果pushStack    return this.add( selector == null ?        this.preObject :         // 可做一次过滤        this.prevObject.filter( selector )    );}



看到这,说明你真的是个有耐心的boy了。我们上主菜!


IE内存泄露

讲data缓存前,首先要必须介绍一下IE6-8的内存泄露问题。IE低版本dom采用COM组件的形式编写,垃圾清理机制,使用的引用计数的方式,无法正确的处理包含dom元素的环状的循环引用。即使刷新页面内存也不会被释放。

通常是如何产生循环引用的呢?

此例中,当前作用域里包含变量a (引用了dom元素),通过绑定事件onclick,使dom引用了事件函数。但是函数在声明时,会把当前所在活动对象的作用域链保存在自身的scope属性,从而引用了当前环境定义的所有变量,而变量a指向dom,从而产生循环引用。需要手动把变量对dom的引用解除

{ // 某个scope作用域    var a = document.getElementById('id_a');    // dom -> fun    a.onclick = function(e) {    };    // fun -> a -> dom , 解除对dom元素的引用, fun -> a -X- dom    a = null;}


jQuery.data原理

jq内部实现了一个缓存机制,用于分离dom对函数等对象的引用(函数对dom的引用取决于函数本身的特性、定义的位置,这个没法子)。如何能做到呢?与 策略模式 的思路一致——字符串约定。

jQuery.extend({    // #247, 构造一个基本不可能被占用的属性, 除去"."    expando: "jQuery" + ( version + Math.random() ).replace(/\D/g, ""),    // #507, 全局使用的 guid 号码    guid: 1,    // #4014, 缓存空间    cache: {}});/* ---- 原理示意 ---- */var a = document.getElementById("id_a");// 每个元素的 jQuery.expando 分到唯一的 guid 号码a[jQuery.expando] = a[jQuery.expando] || jQuery.guid++;// jQuery.cache 上元素的 guid 对应的位置为该对象的缓存空间jQuery.cache[a[jQuery.expando]] = {};

通过 jQuery.data 的封装可以轻松实现数据的存取,但是这样就结束了么?太天真了!

这样做也会产生衍生的问题。虽然不产生循环引用,但对象绑定在jQuery.cache上,即使dom对象被移除,数据仍然不会因此消失。而且如果是函数,仍然会通过引用环境的变量单向引用着dom,导致dom元素也不消失。虽然刷新后内存会清出,不如循环引用严重,但也是问题了。

看起来仍然需要手动设置变量为null,仿佛回到原点,但是数据还在,比之前更不如。解决方法其实很简单,就是移除节点时调用 jQuery.cleanData ,data没有被任何对象引用,自然的回收。

但是问题仍然没解决,因为例如绑定事件,即使函数放在jQuery.cache中,也至少有一个触发函数绑定在dom上,因此 jQuery.event.add( elem, types, handler, data, selector ) 中的elem返回前被设为null ,见源码 #4961。所以如非事事通明,尽量使用jq提供的方式删除节点、绑定事件等

function remove( elem, selector, keepData ) { // #6107, #6255为正式方法    var node,        elems = selector ? jQuery.filter( selector, elem ) : elem,        i = 0;    for ( ; ( node = elems[ i ] ) != null; i++ ) {        if ( !keepData && node.nodeType === 1 ) {            // 清除数据            jQuery.cleanData( getAll( node ) );        }        if ( node.parentNode ) {            if ( keepData && jQuery.contains( node.ownerDocument, node ) ) {                setGlobalEval( getAll( node, "script" ) );            }            // 删除节点            node.parentNode.removeChild( node );        }    }    return elem;}


jQuery架构方法

jQuery在此之上考虑了几点:

核心

  1. 对普通对象和dom对象做了区分。因为普通对象的垃圾回收机制(GC)使用的不是引用计数,不会内存泄露。其次data机制本身增加了复杂度不说,对象解除引用后绑定的数据还需要手动销毁,反而造成内存的无端占用。

    普通对象绑定在自身的 jQuery.expando 属性上,初始化为 { toJSON: jQuery.noop },防止序列化时暴露数据。dom对象绑定在 jQuery.cache[ ele[jQuery.expando] ] ,初始化为 {}

  2. 私用和公用隔离,如 jQuery.cache[ guid ]、jQuery.cache[ guid ].data。内部的事件、队列等等都使用的私有空间调用 _data 方法,而用户储存数据调用的 data 方法则是私有,由参数 pvt 决定,true代表私有

  3. 当移除数据后,jQuery.cache[ guid ]或 jQuery.cache[ guid ].data对象不再有数据,则移除该对象释放内存。且当移除缓存对象时,绑定在elem的事件函数也将被移除

特性

  1. 支持通过简单的自定义配置来增加不支持缓存的特例类型

  2. 扩展支持读取 elem.attributes 中 data- 前缀定义的数据

  3. 以小驼峰书写为标准读取方式,内部进行相应转换

jQuery.data结构

使用常见的外观模式,定义核心功能,通过再封装实现个性化的使用规则,以供其他模块或外部调用

  • 核心功能:(特点:参数多而全,逻辑负责全面)

    jQuery.internalData( elem, name, data, pvt ) 为dom和对象设置data数据,支持 -> 核心12jQuery.internalRemoveData( elem, name, pvt ) 移除dom或对象上指定属性的data数据-> 核心3jQuery.cleanData( elems, forceAcceptData ) 由于删除data时还要删除elem本身上绑定的触发事件函数,因此不能简单 delete cache[id]。单独封装,被事件系统和 jQuery.internalRemoveData使用 ->核心3
  • 外观:(工具方法、实例方法)

    jQuery.hasData( elem ) 直接在对应的 `cache` 中查找。jQuery._data( elem, name, data )  jQuery.\_removeData( elem, name )`用于内部私有调用,封装了核心方法,简化了传参数量。jQuery.data( elem, name, data )  jQuery.removeData( elem, name ) 用于外部公共调用,封装了核心方法,简化了传参数量。jQuery.fn.data( key, value )  jQuery.fn.removeData( key, value ) 封装了 jQuery.data  jQuery.removeData ,遍历this来分别调用
  • 钩子:埋在某个环节直接执行,根据需要实现终止流程、改变特性的功能,或不产生任何影响

    acceptData( elem ) #3762,特性1,过滤dataAttr( elem, key, data ) #3780,特性2,data的值不存在则会访问元素的attribute,返回data,且值会缓存到cachejQuery.camelCase( string ) #356,特性3,小驼峰

[核心 + 外观 + 钩子] 感觉应该算一种常见好用的架构方式了。jq中用到了很多 jQuery.some() -> 核心, jQuery.fn.some() -> 外观 的形式,也都差不多。

这里提一下我所理解的钩子,Callback的四个字符串特性就类似于四个钩子,在最常见的几个需求分歧上埋设,发展了观察者模式的4种特性支持,event的众多钩子 标准方式转换 + 个例bug修正 完成了兼容的大业与主逻辑的统一。

另外不得不提一个在此维度之外的优化方案 —— 提炼重复代码。核心逻辑是不可省的。通常支持数种方式定义参数,把其变化为某种固定形式的参数传入来执行递归是分离函数内部个性化与核心逻辑的第一步,还可以进一步,抽出此部分逻辑,保持主逻辑的纯粹,变成外观部分内的逻辑进行 功能增强。jq再进了一步,由于大量方法都支持参数判断存取、对象或字符串均可、map映射,分离出 access( elems, fn, key, value, chainable, emptyGet, raw ) #4376,核心 jQuery.xxx(),外观jQuery.fn.xxx() 里调用 access`。

// access( elems, fn, key, value, chainable, emptyGet, raw )// 直观语义是让forEach( elems, fn( elem, key, value ) ), 支持类型判断实现映射等功能css: function( name, value ) {  // #7340, 随意举的例子        // 第二个参数要么是 jQuery.someThing        // 或者是封装了 jQuery.someThing.apply( ... ) 的函数,内含        return access( this, function( elem, name, value ) {            var styles, len,                map = {},                i = 0;            // 新增一样只针对css的个性化参数处理            if ( jQuery.isArray( name ) ) {                styles = getStyles( elem );                len = name.length;                for ( ; i < len; i++ ) {                    map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );                }                return map;            }            return value !== undefined ?                jQuery.style( elem, name, value ) :                jQuery.css( elem, name );        }, name, value, arguments.length > 1 );    }

但是,data缓存系统并没有这样使用。原因也很简单,基础的共性部分支持不同不支持映射,如上面的css是共性相同的情况下可以增加个性,但不同的情况就要重新抽出、或写在外观里、或写在核心代码里使用递归。



[源码]

/* ---------------------------------- 1. 相关代码(扫一下) ---------------------------------- */jQuery.extend({    // #247, 构造一个基本不可能被占用的属性, 除去"."    expando: "jQuery" + ( version + Math.random() ).replace(/\D/g, ""),    // #507, 全局使用的 guid 号码    guid: 1,    // #4014, 缓存空间    cache: {},    noData: {        // 为 true 的不可以        "applet ": true,        "embed ": true,        // ...but Flash objects (which have this classid) *can* handle expandos        "object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"    },});// #3748, support.deleteExpando( function() {    var div = document.createElement( "div" );    // Support: IE<9    support.deleteExpando = true;    try {        delete div.test;    } catch ( e ) {        support.deleteExpando = false;    }    // Null elements to avoid leaks in IE.    div = null;} )();// #284, 可用于检测 elem 的公有 cache.data 是否已空jQuery.isEmptyObject = function( obj ) {    var name;    for ( name in obj ) {        return false;    }    return true;};// #3814, 检测 elem 的私有 cache 缓存是否已空function isEmptyDataObject( obj ) {    var name;    for ( name in obj ) {        // if the public data object is empty, the private is still empty        if ( name === "data" && jQuery.isEmptyObject( obj[ name ] ) ) {            continue;        }        if ( name !== "toJSON" ) {            return false;        }    }    return true;}/* ---------------------------------- 2. 钩子(瞅瞅) ---------------------------------- */// #3762var acceptData = function( elem ) {    // 比对禁止列表 jQuery.noData, 为 true 必然不能    var noData = jQuery.noData[ ( elem.nodeName + " " ).toLowerCase() ],        // 普通对象都会按 nodeType = 1 处理,通过筛选        nodeType = +elem.nodeType || 1;    return nodeType !== 1 && nodeType !== 9 ?        false :        // noData不存在(黑名单) 或 存在且classid与noDta相同但不为true(白名单)        !noData || noData !== true && elem.getAttribute( "classid" ) === noData;};function dataAttr( elem, key, data ) {    // 正确姿势: dataAttr( elem, key, jQuery.data( elem )[ key ] );    // data 不存在,则查找 HTML5 data-* attribute,并存入 jQuery.data( elem, key, data )    // 返回 data    if ( data === undefined && elem.nodeType === 1 ) {        var name = "data-" + key.replace( /([A-Z])/g, "-$1" ).toLowerCase();        data = elem.getAttribute( name );        if ( typeof data === "string" ) {            try {                // "true" -> true , "false" -> false , "23" -> 23                // "{ \"a\": 1}" -> {"a": 1}, "[1, '2']" -> [1, '2']                // 或 data 本身                data = data === "true" ? true :                    data === "false" ? false :                    data === "null" ? null :                    +data + "" === data ? +data :                    /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/.test( data ) ? jQuery.parseJSON( data ) :                    data;            } catch ( e ) {}            // 保存            jQuery.data( elem, key, data );        } else {            data = undefined;        }    }    return data;}// #356, 正则变量替换了,本身在 #80jQuery.camelCase = function( string ) {    // -ms-abc -> msAbc , a-abc -> aAbc    return string.replace( /^-ms-/, "ms-" ).replace( /-([\da-z])/gi, fcamelCase );};/* ---------------------------------- 3. 核心(关键) ---------------------------------- */// #3830, 添加缓存function internalData( elem, name, data, pvt /* true 为私,存cache;false 为公,存cache.data */ ) {    // 钩子,黑白名单    if ( !acceptData( elem ) ) {        return;    }    var ret, thisCache,        internalKey = jQuery.expando,        // dom 和 object 区分对待        isNode = elem.nodeType,        // object 不缓存,存自身 jQuery.expando 属性上        cache = isNode ? jQuery.cache : elem,        // 没设置过说明 第一次        id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;    // 1. 第一次只能写,不能读。否则 return    if ( ( !id || !cache[ id ] || ( !pvt && !cache[ id ].data ) ) &&        data === undefined && typeof name === "string" ) {        return;    }    // 2. 第一次写,先初始化 [ 属性、缓存对象 cache ]    // dom -> jQuery.cache[ elem[ internalKey ] ],object -> object[internalKey]    if ( !id ) {        // Only DOM nodes need a new unique ID for each element since their data        // ends up in the global cache        if ( isNode ) {            id = elem[ internalKey ] = deletedIds.pop() || jQuery.guid++;        } else {            id = internalKey;        }    }    if ( !cache[ id ] ) {        cache[ id ] = isNode ? {} : { toJSON: jQuery.noop };    }    // 3. 写入与读取    // write特例:支持 name 参数为 { "key" : value } 或 函数。存入数据    if ( typeof name === "object" || typeof name === "function" ) {        // 公私不同        if ( pvt ) {            cache[ id ] = jQuery.extend( cache[ id ], name );        } else {            cache[ id ].data = jQuery.extend( cache[ id ].data, name );        }    }    thisCache = cache[ id ];    // thisCache 根据pvt索引到了应该被赋值的对象    if ( !pvt ) {        if ( !thisCache.data ) {            thisCache.data = {};        }        thisCache = thisCache.data;    }    // 写入,小驼峰为标准    if ( data !== undefined ) {        thisCache[ jQuery.camelCase( name ) ] = data;    }    // Check for both converted-to-camel and non-converted data property names    // If a data property was specified    if ( typeof name === "string" ) {        // 这不是重点,也可以读非驼峰。正常使用并不会有        ret = thisCache[ name ];        if ( ret == null ) {            // 读这里,无论读写,都要读一次            ret = thisCache[ jQuery.camelCase( name ) ];        }    } else {        // 无 name , 直接读 cache        ret = thisCache;    }    return ret;}// #3922, 删除 公/私 缓存属性 或 缓存。// 删完属性若变空cache,将移去。会处理掉 elem 上 绑定的 eventfunction internalRemoveData( elem, name, pvt ) {    // 钩子,黑名单者,直接 return    if ( !acceptData( elem ) ) {        return;    }    var thisCache, i,        isNode = elem.nodeType,        cache = isNode ? jQuery.cache : elem,        id = isNode ? elem[ jQuery.expando ] : jQuery.expando;    // 缓存空间未初始化,return    if ( !cache[ id ] ) {        return;    }    // 1. name 存在,删除 name 属性值    if ( name ) {        thisCache = pvt ? cache[ id ] : cache[ id ].data;        if ( thisCache ) {            // 1.1 支持数组定义多属性,此处把字符串形式也转为数组[name]            // next step: 统一迭代删除            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 {                // If "name" is an array of keys...                // When data is initially created, via ("key", "val") signature,                // keys will be converted to camelCase.                // Since there is no way to tell _how_ a key was added, remove                // both plain key and camelCase key. #12786                // This will only penalize the array argument path.                name = name.concat( jQuery.map( name, jQuery.camelCase ) );            }            // 1.2 删            i = name.length;            while ( i-- ) {                delete thisCache[ name[ i ] ];            }            // 1.3 如果 cache 删除后没空,结束 return            // 如果空了, 与 name 不存在的情况一样直接删除 data            if ( pvt ? !isEmptyDataObject( thisCache ) : !jQuery.isEmptyObject( thisCache ) ) {                return;            }        }    }    // 2. 根据 pvt 判断,false 删除公有    if ( !pvt ) {        delete cache[ id ].data;        // cache 还没空,可以闪了,return        // cache 空了,合并到 pvt 为true,私有cache 删除        if ( !isEmptyDataObject( cache[ id ] ) ) {            return;        }    }    // 3. pvt 为 true, 删除私有 cache    // 3.1 为节点时,若cache[events]里还有事件,把 elem 绑定的事件函数删除    if ( isNode ) {        jQuery.cleanData( [ elem ], true );    // 3.2 普通对象时    } else if ( support.deleteExpando || cache != cache.window ) {        // 能删则 delete        delete cache[ id ];    // 不能删, undefined    } else {        cache[ id ] = undefined;    }}// #6192, 函数内包含 删除事件队列时删除elem对应type的绑定事件 的功能// cleanData 不仅被 internalRemoveData 内部调用,remove节点的时候也$().remove调用,因此支持了elems 数组jquery.cleanData = function( elems, /* internal */ forceAcceptData ) {    var elem, type, id, data,        i = 0,        internalKey = jQuery.expando,        cache = jQuery.cache,        attributes = support.attributes,        special = jQuery.event.special;    // 支持 elems 数组迭代    for ( ; ( elem = elems[ i ] ) != null; i++ ) {        // 钩子        if ( forceAcceptData || acceptData( elem ) ) {            id = elem[ internalKey ];            data = id && cache[ id ];            if ( data ) {                // 1. 存在事件队列                if ( data.events ) {                    // 迭代,删除绑在 elem 上的触发函数                    for ( type in data.events ) {                        // 虽然绑定在data上的事件,都转换成标准的 eventType                        // 但标准的 eventtype 可能不被兼容                        // special.setup 钩子在绑定触发函数时会hack一次                        // 需要该方法找到绑定在elem上事件触发函数的真正类型并删除                        if ( special[ type ] ) {                            jQuery.event.remove( elem, type );                        // 一般情况,直接删除                        } else {                            jQuery.removeEvent( elem, type, data.handle );                        }                    }                }                // 2. 不存在(或已删去)事件队列                if ( cache[ id ] ) {                    delete cache[ id ];                    // Support: IE<9                    // IE does not allow us to delete expando properties from nodes                    // IE creates expando attributes along with the property                    // IE does not have a removeAttribute function on Document nodes                    if ( !attributes && typeof elem.removeAttribute !== "undefined" ) {                        elem.removeAttribute( internalKey );                    } else {                        elem[ internalKey ] = undefined;                    }                    deletedIds.push( id );                }            }        }    }}/* ---------------------------------- 4. 外观(接口API) ---------------------------------- */// #4013jQuery.extend( {    // cache、noData 上面已经提前写了    cache: {},    noData: {        "applet ": true,        "embed ": true,        "object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"    },    // 直接从缓存 cache 查找判断,dom 与 object区别对待    hasData: function( elem ) {        elem = elem.nodeType ? jQuery.cache[ elem[ jQuery.expando ] ] : elem[ jQuery.expando ];        return !!elem && !isEmptyDataObject( elem );    },    // 公用,pvt 无    data: function( elem, name, data ) {        return internalData( elem, name, data );    },    removeData: function( elem, name ) {        return internalRemoveData( elem, name );    },    // 私用,pvt true    _data: function( elem, name, data ) {        return internalData( elem, name, data, true );    },    _removeData: function( elem, name ) {        return internalRemoveData( elem, name, true );    }} );// #4049,实例方法jQuery.fn.extend( {    data: function( key, value ) {        var i, name, data,            elem = this[ 0 ],            attrs = elem && elem.attributes;        // Special expections of .data basically thwart jQuery.access,        // so implement the relevant behavior ourselves        // 1. key不存在。获得 data缓存 所有值        if ( key === undefined ) {            if ( this.length ) {                data = jQuery.data( elem );                if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) {                    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 ) );                                // 钩子,data[name]若无,则搜索data- attribute,并赋值给 data[ name ]                                dataAttr( elem, name, data[ name ] );                            }                        }                    }                    jQuery._data( elem, "parsedAttrs", true );                }            }            return data;        }        // 2. key 是 "object"        // internalData 已经支持了 "object" 参数,因此直接迭代        if ( typeof key === "object" ) {            return this.each( function() {                jQuery.data( this, key );            } );        }        return arguments.length > 1 ?            // 3. 迭代写入 key -> value            this.each( function() {                jQuery.data( this, key, value );            } ) :            // 4. 读取 key 属性值, 没有则尝试读data- attribute,并赋值给 data[ name ]            elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : undefined;    },    removeData: function( key ) {        return this.each( function() {            jQuery.removeData( this, key );        } );    }} );



最后再说几点:

  1. $(elem).data("key", "name")$.data($(elem), "key", "name") 的区别是前者内部元素被迭代,绑定在元素(dom)上,而后者绑定在 $(elem) 对象(object)上,区别不言自明。

  2. 对于支持对象等多种参数形式的逻辑本身更多放在外观里,这里在 internalData,因为公有私有不止一个外观,避免重复要么抽出类似 access 使用,要么放到公共方法中。而之所以不放在最终的实例 data 方法中,因为工具方法已经在jq模块内部被多次使用了,这样可以有效简化内部操作。

  3. dataAtrr 之所以在实例 data 方法中才出现被使用,是因为只有用户调用的时候dom才加载完呀,才会产生这个需要。

2 0