jQuery 源码框架解析

参考资料:饥人谷任务班任务42视频jQuery 2.0.3 源码分析core - 整体架构

$调用方法的实现

jQuery 的每一个实例并没有通过 new 来新建,而是使用 $jQuery 来新增实例并直接进行方法的调用,如:

$(".btn").on("click", function() {
    console.log("do sth.");
});
jQuery.each(function() {
    console.log("do sth.");
});

为使每次调用 $jQuery 能够创建实例,在构建 jQuery 对象时,返回值应该是对象的实例。

下面尝试以 rQuery 进行重现。

var rQuery = function(selector, context) {
    //返回实例
}
rQuery.prototype = {};

如果直接使用 new 创建实例,会产生死循环:

var rQuery = function(selector, context) {
    return new rQuery; //没有尽头的递归
}
rQuery.prototype = {};

因此,要想返回正确的实例,必须考虑其他的途径。由于 new 需要针对方法来执行,因此通过返回 rQuery.prototype 来创建实例也是不可行的。但我们可以在原型中新增一个方法,尝试通过返回这个方法来创建实例:

var rQuery = function(selector, context) {
    return new rQuery.prototype.init(); //返回实例
}
rQuery.prototype = {
    init: function() {
        return this; // this 指向实例,也就是 rQuery
    }
};

不过,这种方法会带来一个新的问题(如图):所返回的实例和 rQuery 对象并没有对应的关系(淡粉色线),新的实例不能调用 rQuery 中的方法。

var rQuery = function(selector, context) {
    return new rQuery.prototype.init(); //返回实例
}
rQuery.prototype = {
    init: function() {
        return this;
    }
    print: function() {
        console.log(this);
    }
};

rQuery.print(); // 报错

因此,必须显性地将 init 方法的原型定义为 rQuery 的原型(红色线):

rQuery/init 的原型关系图
var rQuery = function(selector, context) {
    return new rQuery.prototype.init(); //返回实例
}
rQuery.prototype = {
    init: function() {
        return this;
    }
};

rQuery.prototype.init.prototype = rQuery.prototype;

这样,每次使用 rQuery 时就会产生新的实例,并且可以正常使用 rQuery 原型中定义的方法。

定义 window.$ = window.rQuery = rQuery 即可使用 $ 代替 rQuery 进行操作。

链式调用的实现

在 jQuery 中,可以对方法进行链式调用。使用链式调用节约代码、提高效率,简单优雅,可谓 jQuery 的一大魅力。

$(this).removeClass("active").addClass("bye");

要能够进行链式调用,则必须在第一个方法的调用之后返回对象本身:

var $tmp = $(this).removeClass("active"); //$tmp === $(this)

$tmp.addClass("bye"); //相当于 $(this).addClass("bye")

因此,在定义可以使用链式调用的方法时,需要添加 return this; 以保证链式调用能够实现:

var rQuery = function(selector, context) {
    return new rQuery.prototype.init(); //返回实例
}
rQuery.prototype = {
    init: function() {
        return this;
    },
    setName: function(name) {
        this.name = name;
        return this;
    },
    printName: function(name) {
        console.log(this.name);
        return this;
    }
};

rQuery.prototype.init.prototype = rQuery.prototype;

rQuery().setName("Hanna").printName(); //Hanna

插件接口的定义与实现

同其他库一样,jQuery 提供可以让开发者添加扩展的接口。开发者可以通过 jQuery.fn.extend() 来向 jQuery 中添加新的方法。

jQuery 中,同时存在 jQuery.extend()jQuery.fn.extend() 两种方法,从源码中可以发现,二者实际上是指向同一个方法的不同引用:

jQuery.extend = jQuery.fn.extend = function() {
    //具体实现
}

这样做的用意是:通过 jQuery.extend() 实现对 jQuery 本身的属性和方法的扩展;而通过 jQuery.fn.extend() 来实现对 jQuery.fn,即 jQuery 的原型的属性和方法进行扩展。同时,两种扩展都不会破坏 jQuery 原有的结构。

之所以在使用 jQuery.extend = jQuery.fn.extend的前提下仍能够区别出 jQuery 和 jQuery.fn,是因为对 this 的运用。 this 指向的是调用方法的对象。在 jQuery.extend() 中,调用 extend() 方法的是 jQuery,所以 this 指向 jQuery,而在 jQuery.fn.extend() 中,调用方法的则是 jQuery.fn,因此这里 this 指向了 jQuery.fn,从而成功地区分出了两种不同的扩展方式。

之前曾参考《使用jquery实现简单的拖动效果》实现了一个拖拽效果,拿到这里来试一下 extend() 方法的使用(在线DEMO):

//html
<div class="panel">This div is draggable</div>
//javascript
//drag()
var drag = function() {
    $(this).each(function() {
        var me = $(this);
        var isMoved = false;
        var rX, rY;

        me.on("mousedown", function(event) {
            isMoved = true;
            rX = event.pageX - parseInt(me.css("left"));
            rY = event.pageY - parseInt(me.css("top"));

            me.css("cursor", "move");
            return false; //解决拖拽导致的文字选中问题
        });

        $(document).on("mousemove", function(event) {
            if (isMoved) {
                var eX = event.pageX;
                var eY = event.pageY;
                var wX = $(window).width();
                var wY = $(window).height();
                var thisX = me.width();
                var thisY = me.height();
                var thisMarginLeft = parseInt(me.css("margin-left"));
                var nowX = eX - rX;
                var nowY = eY - rY;
                var left, top;

                var thisLeft = (thisMarginLeft > 0) ? 0 : (-thisMarginLeft);

                if (nowX < thisLeft) {
                    left = thisLeft;
                } else if (nowX > wX - thisX + thisLeft) {
                    left = wX - thisX + thisLeft;
                } else {
                    left = nowX;
                }

                if (nowY < 0) {
                    top = 0;
                } else if (nowY > wY - thisY) {
                    top = wY - thisY;
                } else {
                    top = nowY;
                }

                me.css({
                    "left": left,
                    "top": top
                });
            }
        }).on("mouseup", function() {
            isMoved = false;
            me.css("cursor", "auto");
        });
    });
};
// 添加到 jQuery 中
jQuery.fn.extend({drag: drag});
// 调用
$(".panel").drag();

可以看出,drag() 方法已成功添加到 jQuery 当中,可以用 $(".panel").drag() 进行调用,并实现拖拽功能。

drag() 为例,来看 extend() 方法的实现:

jQuery.extend = jQuery.fn.extend = function() {
  var src, copyIsArray, copy, name, options, clone,
    target = arguments[0] || {}, // 传入的参数,以第一个参数为目标对象(这里是 {drag: drag})
    i = 1,
    length = arguments.length,
    deep = false; // 深拷贝的参数

  // Handle a deep copy situation
  if ( typeof target === "boolean" ) { // 如果第一个参数是 true/false,通常为 true
    deep = target; //deep 为 true/false
    target = arguments[1] || {}; // 将第二个参数作为目标对象
    // skip the boolean and the target
    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
  if ( length === i ) { // 如果只有一个参数
    target = this; // 就将 jQuery(.fn) 自身作为目标对象(这里,因为只传入了 {drag: drag},因此目标对象为 jQuery(.fn),也就实现了向 jQuery 中添加对象(方法))
    --i; // i=0
  }

  for ( ; i < length; i++ ) {
    // Only deal with non-null/undefined values
    if ( (options = arguments[ i ]) != null ) { // 这里 options = {drag: drag}
      // Extend the base object
      for ( name in options ) {
        src = target[ name ]; // 向目标对象(这里是 jQuery(.fn)) 中添加属性,并将属性值赋予变量 src,如果目标对象中没有该属性,那么此时 src = undefined(这里 src = undefined)
        copy = options[ name ]; // copy = drag (drag方法)

        // Prevent never-ending loop
        if ( target === copy ) { // 防止自引用 (这里防止将 jQuery(.fn) 添加到 jQuery(.fn) 中)
          continue;
        }

        // Recurse if we're merging plain objects or arrays
        if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
          // 如果是深拷贝(deep),而且要拷贝的属性值为对象(这里deep = false,不做深拷贝)
          if ( copyIsArray ) { // 如果要拷贝的属性值是数组
            copyIsArray = false;
            clone = src && jQuery.isArray(src) ? src : [];

          } else { // 要拷贝的属性值为普通对象
            clone = src && jQuery.isPlainObject(src) ? src : {}; // clone 为目标对象中的属性值或空对象
          }

          // Never move original objects, clone them
          target[ name ] = jQuery.extend( deep, clone, copy ); // 递归

        // Don't bring in undefined values
        } else if ( copy !== undefined ) { // 这里 drag() 非 undefined
          target[ name ] = copy; // 将属性添加到目标对象中(这里, jQuery(.fn)[drag] = drag)
        }
      }
    }
  }

  // Return the modified object
  return target; // 返回目标对象(这里返回的是添加了 drag() 方法的 jQuery(.fn))
};
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容