小昱个人博客
欢迎来到小昱的世界

勤学如春起之苗,不见其增,日有所长;辍学如磨刀之石,不见其损,日有所亏
jQuery源码研读心得与感悟
  • 首页 > 前端 > jQuery
  • 作者:小昱
  • 2017年8月28日 14:43 星期一
  • 浏览:101
  • 字号:
  • 评论:0
  • 最近在研读jQuery的源码,被其优雅精妙的代码所折服,结构明晰,高内聚、低耦合,兼具优秀的性能与便利的扩展性,让人感受到代码之美。

    其中不得不说以下几点心得:

    一、钩子机制(hook)

    钩子是编程惯用的一种手法,用来解决一种或多种特殊情况的处理。

    钩子的核心原则,保持代码整体逻辑的流畅性
    简单来说,钩子就是适配器原理,或者说是表驱动原理,我们预先定义了一些钩子,在正常的代码逻辑中使用钩子去适配一些特殊的属性,样式或事件,这样可以让我们少写很多 else if 语句。

    举个栗子:

    function match(name, score, bonus) {
        return {
            name: name,
            score: score,
            bonus: bonus
        };
    }
    
    function judge(matches) {
        var result = {};
        for (var i in matches) {
            var match = matches[i];
            var ret = match.score;
            // 判断等级获得相应的加分
            if (match.bonus === 'rank1') {
                ret += 1000;
            } else if (match.bonus === 'rank2') {
                ret += 100;
            } else if (match.bonus === 'rank3') {
                ret += 50;
            }
            result[match.name] = ret;
        }
        return result;
    }
      
    var zhao = match("zhao", 10, 'rank0');
    var qian = match('qian', 8, 'rank1');
    var shun = match('shun', 60, 'rank2');
    var li = match('li', 60, 'rank3');
    
    var result = judge([zhao, qian, shun, li]);
    
    console.log(result);

    可以看到,上例使用了多个else来判断等级,业务少的时候还行,但如果业务多的时候,一个一个书写显得冗余麻烦,也增加了代码维护的困难,成本很大。那么这个时候如果使用钩子机制:

    function match(name, score, bonus) {
        return {
            name: name,
            score: score,
            bonus: bonus
        };
    }
    
    var rankMap = {
      rank1: 1000,
      rank2: 100,
      rank3: 50
    }
    
    function judge(matches) {
        var result = {};
        for (var i in matches) {
            var match = matches[i];
            var ret = match.score;
            // 判断是否存在相应等级的加分
            if (rankMap[match.bonus]) {
                ret += rankMap[match.bonus];
            }
            result[match.name] = ret;
        }
        return result;
    }
      
    var zhao = match("zhao", 10, 'rank0');
    var qian = match('qian', 8, 'rank1');
    var shun = match('shun', 60, 'rank2');
    var li = match('li', 60, 'rank3');
    
    var result = judge([zhao, qian, shun, li]);
    
    console.log(result);

    可以看到,使用钩子去处理特殊情况,可以让代码的逻辑更加清晰,省去大量的条件判断,上面的钩子机制的实现方式,采用的就是表驱动方式,就是我们事先预定好一张表(俗称打表),用这张表去适配特殊情况。

    当然 jQuery 的 hook 是一种更为抽象的概念,在不同场景可以用不同方式实现。

    比如,jQuery的$.type的实现方式:

    (function(window, undefined) {
        var class2type = {}; // 用于预存储一张类型表用于 hook
    
        core_toString = class2type.toString;
     
        // 原生的 typeof 方法并不能区分出一个变量它是 Array 、RegExp 等 object 类型,jQuery 为了扩展 typeof 的表达力,因此有了 $.type 方法
        // 针对一些特殊的对象(例如 null,Array,RegExp)也进行精准的类型判断
        // 运用了钩子机制,判断类型前,将常见类型打表,先存于一个 Hash 表 class2type 里边
        jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
            class2type["[object " + name + "]"] = name.toLowerCase();
        });
     
        jQuery.extend({
            // 确定JavaScript 对象的类型
            // 这个方法的关键之处在于 class2type[core_toString.call(obj)]
            // 可以使得 typeof obj 为 "object" 类型的得到更进一步的精确判断
            type: function(obj) {
     
                if (obj == null) {
                    return String(obj);
                }
                // 利用事先存好的 hash 表 class2type 作精准判断
                // 这里因为 hook 的存在,省去了大量的 else if 判断
                return typeof obj === "object" || typeof obj === "function" ?
                    class2type[core_toString.call(obj)] || "object" :
                    typeof obj;
            }
        })
    })(window);

    我们可以模拟实现一下:

    var class2type = {};
    core_toString = class2type.toString;
    $.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
        class2type["[object " + name + "]"] = name.toLowerCase();
    });
    
    console.log(class2type);
    
    function type(obj) {
      if (obj == null) {
          return String(obj);
      }
      // 利用事先存好的 hash 表 class2type 作精准判断
      // 这里因为 hook 的存在,省去了大量的 else if 判断
      return typeof obj === "object" || typeof obj === "function" ?
          class2type[core_toString.call(obj)] || "object" :
          typeof obj;
    }
    
    console.log(type(1));
    console.log(type(''));
    console.log(type(true));
    console.log(type([]));
    console.log(type(/a/));
    console.log(type({}));
    console.log(type(function(){}));

    从某种程度上讲,钩子是一系列被设计为以你自己的代码来处理自定义值的回调函数。有了钩子,你可以将差不多任何东西保持在可控范围内。

    从设计模式的角度而言,这种钩子运用了策略模式。

    策略模式:将不变的部分和变化的部分隔开是每个设计模式的主题,而策略模式则是将算法的使用与算法的实现分离开来的典型代表。使用策略模式重构代码,可以消除程序中大片的条件分支语句。在实际开发中,我们通常会把算法的含义扩散开来,使策略模式也可以用来封装一系列的“业务规则”。只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以使用策略模式来封装他们。

    策略模式的优点:

    • 策略模式利用组合,委托和多态等技术思想,可以有效的避免多重条件选择语句;
    • 策略模式提供了对开放-封闭原则的完美支持,将算法封装在独立的函数中,使得它们易于切换,易于理解,易于扩展。
    • 策略模式中的算法也可以复用在系统的其它地方,从而避免许多重复的复制粘贴工作。

     

    二、短路表达式

    短路表达式这个应该人所皆知了。在 jQuery 中,大量的使用了短路表达式与多重短路表达式。

    短路表达式:作为"&&"和"||"操作符的操作数表达式,这些表达式在进行求值时,只要最终的结果已经可以确定是真或假,求值过程便告终止,这称之为短路求值。这是这两个操作符的一个重要属性。

    // ||短路表达式
    var foo = a || b;
    // 相当于
    if (a) {
        foo = a;
    } else {
        foo = b;
    }
     
    // &&短路表达式
    var bar = a && b;
    // 相当于
    if (a) {
        bar = b;
    } else {
        bar = a;
    }

    使用断路表达式代码很优雅,但是可读性下降了很多,使用的时候权衡一下,多重短路表达式和简单短路表达式其实一样,只需要先把后面的当成一个整体,依次推进,得出最终值。

    看如下多短路路表达式:

    var a = 1, b = 0, c = 3;
     
    var foo = a && b && c, // 0 ,相当于 a && (b && c)
        bar = a || b || c;  // 1

    这里需要提出一些值得注意的点:

    1、在 Javascript 的逻辑运算中,0、""、null、false、undefined、NaN 都会判定为 false ,而其他都为 true;

    2、因为 Javascript 的内置弱类型域 (weak-typing domain),所以对严格的输入验证这一点不太在意,即便使用 && 或者 || 运算符的运算数不是布尔值,仍然可以将它看作布尔运算。虽然如此,还是建议如下:

    if(foo){ ... }     //不够严谨
    if(!!foo){ ... }   //更为严谨,!!可将其他类型的值转换为boolean类型

    注重细节,JavaScript 既不弱也不低等,我们只是需要更努力一点工作以使我们的代码变得真正健壮。

     

    三、链式调用及回溯

    另一个让大家喜爱使用 jQuery 的原因是它的链式调用,这一点的实现其实很简单,只需要在要实现链式调用的方法的返回结果里,返回 this ,就能够实现链式调用了。

    当然,除了链式调用,jQuery 甚至还允许回溯,如:

    // 通过 end() 方法终止在当前链的最新过滤操作,返回上一个对象集合
    $('div').eq(0).show().end().eq(1).hide();

    当选择了 ('div').eq(0) 之后使用 end() 可以回溯到上一步选中的 jQuery 对象 $('div'),其内部实现其实是依靠添加了 prevObject 这个属性

    jQuery 完整的链式调用、增栈、回溯通过 return this 、 return this.pushStack() 、return this.prevObject 实现,看看源码实现:

    jQuery.fn = jQuery.prototype = {
        // 将一个 DOM 元素集合加入到 jQuery 栈
        // 此方法在 jQuery 的 DOM 操作中被频繁的使用, 如在 parent(), find(), filter() 中
        // pushStack() 方法通过改变一个 jQuery 对象的 prevObject 属性来跟踪链式调用中前一个方法返回的 DOM 结果集合
        // 当我们在链式调用 end() 方法后, 内部就返回当前 jQuery 对象的 prevObject 属性
        pushStack: function(elems) {
            // 构建一个新的jQuery对象,无参的 this.constructor(),只是返回引用this
            // jQuery.merge 把 elems 节点合并到新的 jQuery 对象
            // this.constructor 就是 jQuery 的构造函数 jQuery.fn.init,所以 this.constructor() 返回一个 jQuery 对象
            // 由于 jQuery.merge 函数返回的对象是第二个函数附加到第一个上面,所以 ret 也是一个 jQuery 对象,这里可以解释为什么 pushStack 出入的 DOM 对象也可以用 CSS 方法进行操作
            var ret = jQuery.merge(this.constructor(), elems);
     
            // 给返回的新 jQuery 对象添加属性 prevObject
            // 所以也就是为什么通过 prevObject 能取到上一个合集的引用了
            ret.prevObject = this;
            ret.context = this.context;
     
            // Return the newly-formed element set
            return ret;
        },
        // 回溯链式调用的上一个对象
        end: function() {
            // 回溯的关键是返回 prevObject 属性
            // 而 prevObject 属性保存了上一步操作的 jQuery 对象集合
            return this.prevObject || this.constructor(null);
        },
        // 取当前 jQuery 对象的第 i 个
        eq: function(i) {
            // jQuery 对象集合的长度
            var len = this.length,
                j = +i + (i < 0 ? len : 0);
     
            // 利用 pushStack 返回
            return this.pushStack(j >= 0 && j < len ? [this[j]] : []);
        }, 
    }

    总的来说,

    1)end() 方法返回 prevObject 属性,这个属性记录了上一步操作的 jQuery 对象合集;

    2)而 prevObject 属性由 pushStack() 方法生成,该方法将一个 DOM 元素集合加入到 jQuery 内部管理的一个栈中,通过改变 jQuery 对象的 prevObject 属性来跟踪链式调用中前一个方法返回的 DOM 结果集合

    3)当我们在链式调用 end() 方法后,内部就返回当前 jQuery 对象的 prevObject 属性,完成回溯。

     

    四、命令查询同体

    就是函数重载。正常而言,应该是命令查询分离(Command and Query Separation,CQS),是源于命令式编程的一个概念。那些改变对象的状态(内部的值)的函数称为命令,而那些检索值的函数称为查询。原则上,查询函数返回数据,命令函数返回状态,各司其职。而 jQuery 将 getter 和 setter 方法压缩到单一方法中创建了一个连贯的接口,使得代码暴露更少的方法,但却以更少的代码实现同样的目标。

    // 获取 title 属性的值
    $('#id').attr('title');
    // 设置 title 属性的值
    $('#id').attr('title','jQuery');
     
    // 获取 css 某个属性的值
    $('#id').css('title');
    // 设置 css 某个属性的值
    $('#id').css('width','200px');

    直接实例化一个 jQuery 对象,就有着 9 种不同的方法重载场景:

    // 接受一个字符串,其中包含了用于匹配元素集合的 CSS 选择器
    jQuery([selector,[context]])
    // 传入单个 DOM
    jQuery(element)
    // 传入 DOM 数组
    jQuery(elementArray)
    // 传入 JS 对象
    jQuery(object)
    // 传入 jQuery 对象
    jQuery(jQuery object)
    // 传入原始 HTML 的字符串来创建 DOM 元素
    jQuery(html,[ownerDocument])
    jQuery(html,[attributes])
    // 传入空参数
    jQuery()
    // 绑定一个在 DOM 文档载入完成后执行的函数
    jQuery(callback)

     

    五、参数映射及处理

    jQuery 的接口连贯性还体现在了对参数的兼容处理上,方法如何接收数据比让它们具有可链性更为重要。虽然方法的链式调用是非常普遍的,你可以很容易地在你的代码中实现,但是处理参数却不同,使用者可能传入各种奇怪的参数类型,而 jQuery 作者想的真的很周到,考虑了用户的多种使用场景,提供了多种对参数的处理。

    // 传入键值对
    jQuery("#some-selector")
      .css("background", "red")
      .css("color", "white")
      .css("font-weight", "bold")
      .css("padding", 10);
     
    // 传入 JSON 对象
    jQuery("#some-selector").css({
      "background" : "red",
      "color" : "white",
      "font-weight" : "bold",
      "padding" : 10
    });

    jQuery 的 on() 方法可以注册事件处理器。和 CSS() 一样它也可以接收一组映射格式的事件,但更进一步地,它允许单一处理器可以被多个事件注册:

    // binding events by passing a map
    jQuery("#some-selector").on({
      "click" : myClickHandler,
      "keyup" : myKeyupHandler,
      "change" : myChangeHandler
    });
     
    // binding a handler to multiple events:
    jQuery("#some-selector").on("click keyup change", myEventHandler);

     

    六、无 new 构造

    // 无 new 构造
    $('#test').text('Test');
     
    // 当然也可以使用 new
    var test = new $('#test');
    test.text('Test');

    大部分人使用 jQuery 的时候都是使用第一种无 new 的构造方式,直接 $('') 进行构造,这也是 jQuery 十分便捷的一个地方。当我们使用第一种无 new 构造方式的时候,其本质就是相当于 new jQuery(),那么在 jQuery 内部是如何实现的呢?

    (function(window, undefined) {
        var
        // ...
        jQuery = function(selector, context) {
            // The jQuery object is actually just the init constructor 'enhanced'
            // 看这里,实例化方法 jQuery() 实际上是调用了其拓展的原型方法 jQuery.fn.init
            return new jQuery.fn.init(selector, context, rootjQuery);
        },
     
        // jQuery.prototype 即是 jQuery 的原型,挂载在上面的方法,即可让所有生成的 jQuery 对象使用
        jQuery.fn = jQuery.prototype = {
            // 实例化化方法,这个方法可以称作 jQuery 对象构造器
            init: function(selector, context, rootjQuery) {
                // ...
            }
        }
        // 这一句很关键,也很绕
        // jQuery 没有使用 new 运算符将 jQuery 实例化,而是直接调用其函数
        // 要实现这样,那么 jQuery 就要看成一个类,且返回一个正确的实例
        // 且实例还要能正确访问 jQuery 类原型上的属性与方法
        // jQuery 的方式是通过原型传递解决问题,把 jQuery 的原型传递给jQuery.prototype.init.prototype
        // 所以通过这个方法生成的实例 this 所指向的仍然是 jQuery.fn,所以能正确访问 jQuery 类原型上的属性与方法
        jQuery.fn.init.prototype = jQuery.fn;
     
    })(window);

    大部分人初看 jQuery.fn.init.prototype = jQuery.fn 这一句都会被卡主,很是不解。但是这句真的算是 jQuery 的绝妙之处。理解这几句很重要,分点解析一下:

    1)首先要明确,使用 $('xxx') 这种实例化方式,其内部调用的是 return new jQuery.fn.init(selector, context, rootjQuery) 这一句话,也就是构造实例是交给了 jQuery.fn.init() 方法去完成。

    2)将 jQuery.fn.init 的 prototype 属性设置为 jQuery.fn,那么使用 new jQuery.fn.init() 生成的对象的原型对象就是 jQuery.fn ,所以挂载到 jQuery.fn 上面的函数就相当于挂载到 jQuery.fn.init() 生成的 jQuery 对象上,所有使用 new jQuery.fn.init() 生成的对象也能够访问到 jQuery.fn 上的所有原型方法。

    3)也就是实例化方法存在这么一个关系链  

    • jQuery.fn.init.prototype = jQuery.fn = jQuery.prototype ;
    • new jQuery.fn.init() 相当于 new jQuery() ;
    • jQuery() 返回的是 new jQuery.fn.init(),而 var obj = new jQuery(),所以这 2 者是相当的,所以我们可以无 new 实例化 jQuery 对象。

     

    七、方法扩展 jQuery.fn.extend 与 jQuery.extend

    extend 方法在 jQuery 中是一个很重要的方法,jQuey 内部用它来扩展静态方法或实例方法,而且我们开发 jQuery 插件开发的时候也会用到它。但是在内部,是存在 jQuery.fn.extend 和 jQuery.extend 两个 extend 方法的,而区分这两个 extend 方法是理解 jQuery 的很关键的一部分。先看结论:

    1)jQuery.extend(object) 为扩展 jQuery 类本身,为类添加新的静态方法;

    2)jQuery.fn.extend(object) 给 jQuery 对象添加实例方法,也就是通过这个 extend 添加的新方法,实例化的 jQuery 对象都能使用,因为它是挂载在 jQuery.fn 上的方法(上文有提到,jQuery.fn = jQuery.prototype )。 

    它们的官方解释是:

    1)jQuery.extend(): 把两个或者更多的对象合并到第一个当中,

    2)jQuery.fn.extend():把对象挂载到 jQuery 的 prototype 属性,来扩展一个新的 jQuery 实例方法。

    也就是说,使用 jQuery.extend() 拓展的静态方法,我们可以直接使用 $.xxx 进行调用(xxx是拓展的方法名),

    而使用 jQuery.fn.extend() 拓展的实例方法,需要使用 $().xxx 调用。

    // 扩展合并函数
    // 合并两个或更多对象的属性到第一个对象中,jQuery 后续的大部分功能都通过该函数扩展
    // 虽然实现方式一样,但是要注意区分用法的不一样,那么为什么两个方法指向同一个函数实现,但是却实现不同的功能呢,
    // 阅读源码就能发现这归功于 this 的强大力量
    // 如果传入两个或多个对象,所有对象的属性会被添加到第一个对象 target
    // 如果只传入一个对象,则将对象的属性添加到 jQuery 对象中,也就是添加静态方法
    // 用这种方式,我们可以为 jQuery 命名空间增加新的方法,可以用于编写 jQuery 插件
    // 如果不想改变传入的对象,可以传入一个空对象:$.extend({}, object1, object2);
    // 默认合并操作是不迭代的,即便 target 的某个属性是对象或属性,也会被完全覆盖而不是合并
    // 如果第一个参数是 true,则是深拷贝
    // 从 object 原型继承的属性会被拷贝,值为 undefined 的属性不会被拷贝
    // 因为性能原因,JavaScript 自带类型的属性不会合并
    jQuery.extend = jQuery.fn.extend = function() {
        var src, copyIsArray, copy, name, options, clone,
            target = arguments[0] || {},
            i = 1,
            length = arguments.length,
            deep = false;
     
        // Handle a deep copy situation
        // target 是传入的第一个参数
        // 如果第一个参数是布尔类型,则表示是否要深递归,
        if (typeof target === "boolean") {
            deep = target;
            target = arguments[1] || {};
            // skip the boolean and the target
            // 如果传了类型为 boolean 的第一个参数,i 则从 2 开始
            i = 2;
        }
     
        // Handle case when target is a string or something (possible in deep copy)
        // 如果传入的第一个参数是 字符串或者其他
        if (typeof target !== "object" && !jQuery.isFunction(target)) {
            target = {};
        }
     
        // extend jQuery itself if only one argument is passed
        // 如果参数的长度为 1 ,表示是 jQuery 静态方法
        if (length === i) {
            target = this;
            --i;
        }
     
        // 可以传入多个复制源
        // i 是从 1或2 开始的
        for (; i < length; i++) {
            // Only deal with non-null/undefined values
            // 将每个源的属性全部复制到 target 上
            if ((options = arguments[i]) != null) {
                // Extend the base object
                for (name in options) {
                    // src 是源(即本身)的值
                    // copy 是即将要复制过去的值
                    src = target[name];
                    copy = options[name];
     
                    // Prevent never-ending loop
                    // 防止有环,例如 extend(true, target, {'target':target});
                    if (target === copy) {
                        continue;
                    }
     
                    // Recurse if we're merging plain objects or arrays
                    // 这里是递归调用,最终都会到下面的 else if 分支
                    // jQuery.isPlainObject 用于测试是否为纯粹的对象
                    // 纯粹的对象指的是 通过 "{}" 或者 "new Object" 创建的
                    // 如果是深复制
                    if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
                        // 数组
                        if (copyIsArray) {
                            copyIsArray = false;
                            clone = src && jQuery.isArray(src) ? src : [];
     
                            // 对象
                        } else {
                            clone = src && jQuery.isPlainObject(src) ? src : {};
                        }
     
                        // Never move original objects, clone them
                        // 递归
                        target[name] = jQuery.extend(deep, clone, copy);
     
                        // Don't bring in undefined values
                        // 最终都会到这条分支
                        // 简单的值覆盖
                    } else if (copy !== undefined) {
                        target[name] = copy;
                    }
                }
            }
        }
     
        // Return the modified object
        // 返回新的 target
        // 如果 i < length ,是直接返回没经过处理的 target,也就是 arguments[0]
        // 也就是如果不传需要覆盖的源,调用 $.extend 其实是增加 jQuery 的静态方法
        return target;
    };

    需要注意的是这一句 jQuery.extend = jQuery.fn.extend = function() {} ,也就是 jQuery.extend 的实现和 jQuery.fn.extend 的实现共用了同一个方法,但是为什么能够实现不同的功能了,这就要归功于 Javascript 强大(怪异?)的 this 了。

    1)在 jQuery.extend() 中,this 的指向是 jQuery 对象(或者说是 jQuery 类),所以这里扩展在 jQuery 上;

    2)在 jQuery.fn.extend() 中,this 的指向是 fn 对象,前面有提到 jQuery.fn = jQuery.prototype ,也就是这里增加的是原型方法,也就是对象方法。

     

    八、变量冲突处理 noConflict()

    当需要处理冲突的时候,调用静态方法 noConflict(),让出变量的控制权,源码如下:

    (function(window, undefined) {
        var
            // Map over jQuery in case of overwrite
            // 设置别名,通过两个私有变量映射了 window 环境下的 jQuery 和 $ 两个对象,以防止变量被强行覆盖
            _jQuery = window.jQuery,
            _$ = window.$;
     
        jQuery.extend({
            // noConflict() 方法让出变量 $ 的 jQuery 控制权,这样其他脚本就可以使用它了
            // 通过全名替代简写的方式来使用 jQuery
            // deep -- 布尔值,指示是否允许彻底将 jQuery 变量还原(移交 $ 引用的同时是否移交 jQuery 对象本身)
            noConflict: function(deep) {
                // 判断全局 $ 变量是否等于 jQuery 变量
                // 如果等于,则重新还原全局变量 $ 为 jQuery 运行之前的变量(存储在内部变量 _$ 中)
                if (window.$ === jQuery) {
                    // 此时 jQuery 别名 $ 失效
                    window.$ = _$;
                }
                // 当开启深度冲突处理并且全局变量 jQuery 等于内部 jQuery,则把全局 jQuery 还原成之前的状况
                if (deep && window.jQuery === jQuery) {
                    // 如果 deep 为 true,此时 jQuery 失效
                    window.jQuery = _jQuery;
                }
     
                // 这里返回的是 jQuery 库内部的 jQuery 构造函数(new jQuery.fn.init())
                // 像使用 $ 一样尽情使用它吧
                return jQuery;
            }
        })
    }(window)

    画了一幅简单的流程图帮助理解:

    那么让出了这两个符号之后,是否就不能在我们的代码中使用 jQuery 或者呢 $ 呢?莫慌,还是可以使用的:

    // 让出 jQuery 、$ 的控制权不代表不能使用 jQuery 和 $ ,方法如下:
    var query = jQuery.noConflict(true);
     
    (function($) {
     
    // 插件或其他形式的代码,也可以将参数设为 jQuery
    })(query);
     
    //  ... 其他用 $ 作为别名的库的代码

     

     

     


    参考资料

    【深入浅出jQuery】源码浅析

      您阅读这篇文章共花了:  
    二维码加载中...
    本文作者:小昱      文章标题: jQuery源码研读心得与感悟
    本文地址:http://www.xiaoyulive.top/?post=125
    版权声明:若无注明,本文皆为“小昱个人博客”原创,转载请保留文章出处。
    返回顶部| 首页| 碰碰手气| 捐赠支持| 手机版本|后花园

    Copyright © 2016-2017 小昱个人博客 滇ICP备16006294号