资讯专栏INFORMATION COLUMN

jQuery 源码系列(一)总体架构

svtter / 2288人阅读

摘要:到目前为止,的贡献者团队共名成员,多条,可想而知,是一个多么庞大的项目。参考源码分析整体架构源码解析读书笔记第二章构造对象函数详解本文在上的源码地址,欢迎来。

欢迎来我的专栏查看系列文章。

决定你走多远的是基础,jQuery 源码分析,向长者膜拜!

我虽然接触 jQuery 很久了,但也只是局限于表面使用的层次,碰到一些问题,找到 jQuery 的解决办法,然后使用。显然,这种做法的弊端就是,无论你怎么学,都只能是个小白。

当我建立这个项目的时候,就表示,我要改变这一切了,做一些人想做,憧憬去做,但从没踏入第一步的事情,学习 jQuery 源码。

到目前为止,jQuery 的贡献者团队共 256 名成员,6000 多条 commits,可想而知,jQuery 是一个多么庞大的项目。jQuery 官方的版本目前是 v3.1.1,已经衍生出 jQueryUI、jQueryMobile 等多个项目。

虽然我在前端爬摸打滚一年多,自认基础不是很好,在没有外界帮助的情况下,直接阅读项目源码太难了,所以在边参考遍实践的过程中写下来这个项目。

首先,先推荐一个 jQuery 的源码查询网站,这个网站给初学者非常大的帮助,不仅能查找不同版本的 jQuery 源码,还能索引函数,功能简直吊炸天。

另外,推荐两个分析 jQuery 的博客:

jQuery源码分析系列

原创 jQuery1.6.1源码分析系列(停止更新)

这两个博客给我了很大的帮助,谢谢。

另外还有下面的网址,让我在如何使用 jQuery 上得心应手:

jQuery API 中文文档

jQuery 总体架构

首先,jQuery 是一个开发框架,它的火爆程度已经无法用言语来形容,当你随便打开一个网站,一半以上直接使用了 jQuery。或许,早几年,一个前端工程师,只要会写 jQuery,就可以无忧工作。虽说最近 react、vue 很火,但 jQuery 中许多精彩的方法和逻辑值得每一个前端人员学习。

和其众多的框架一样,总要把接口放到外面来调用,内部往往是一个闭包,避免环境变量的污染。

先来看看 jQuery 使用上的几大特点:

$("#id") 函数方式直接生成 jQuery 对象

$("#id").css().html().hide() 链式调用

关于链式调用,我想有点基础都很容易实现,函数结尾 return this 即可,主要来介绍一下无 new 实现创建对象。

无 new 函数实现

下面是一个普通的函数,很显然,会陷入死循环:

var jQuery = function(){
  return new jQuery();
}
jQuery.prototype = {
  ...
}

这个死循环来的太突然,jQuery() 会创建一个 new jQuery,new jQuery 又会创建一个 new jQuery...

jQuery 用一个 init 函数来代替直接 new 函数名的方式,还要考虑到 jQuery 中分离作用域:

var jQuery = function(){
  return new jQuery.prototype.init();
}
jQuery.prototype = {
  constructor: jQuery,
  init: function(){
    this.jquery = 1.0;
    return this;
  },
  jquery: 2.0,
  each: function(){
    console.log("each");
    return this;
  }
}
jQuery().jquery //1.0
jQuery.prototype.jquery //2.0

jQuery().each() // error

上面看似运行正常,但是问题出在 jQuery().each() // error,访问不到 each 函数。实际上,new jQuery.prototype.init() 返回到是谁的实例?是 init 这个函数的实例,所以 init 函数中的 this 就没了意义。

那么,如果:

var jq = jQuery();
jq.__proto__ === jQuery.prototype;
jq.each === jQuery.prototype.each;

如果可以实现上面的 proto 的指向问题,原型函数调用问题就解决了,但实际上

var jq = jQuery();
jq.__proto__ === jQuery.prototype.init.prototype; //true

实际上,jq 的 proto 是指向 init 函数的原型,所以,我们可以把 jQuery.prototype.init.prototype = jQuery.prototype,这个时候,函数调用就顺理成章了,而且使用的都是引用,指向的都是同一个 prototype 对象,也不需要担心循环问题。实际上,jQuery 就是这么干的。

var jQuery = function(){
  return new jQuery.prototype.init();
}
jQuery.prototype = {
  constructor: jQuery,
  init: function(){
    this.jquery = 1.0;
    return this;
  },
  jquery: 2.0,
  each: function(){
    console.log("each");
    return this;
  }
}
jQuery.prototype.init.prototype = jQuery.prototype;
jQuery().each() //"each"
jQuery 内部结构图

在说内部图之前,先说下 jQuery.fn,它实际上是 prototype 的一个引用,指向 jQuery.prototype 的,

var jQuery = function(){
  return new jQuery.prototype.init();
}
jQuery.fn = jQuery.prototype = {
  ...
}

那么为什么要用 fn 指向 prototype?我本人查阅了一些资料,貌似还是下面的回答比较中肯:简介。你不觉得 fn 比 prototype 好写多了吗。

借用网上的一张图:

从这张图中可以看出,window 对象上有两个公共的接口,分别是 $ 和 jQuery:

window.jQuery = window.$ = jQuery;

jQuery.extend 方法是一个对象拷贝的方法,包括深拷贝,后面会详细讲解源码,暂时先放一边。

下面的关系可能会有些乱,但是仔细看了前面的介绍,应该能看懂。fn 就是 prototype,所以 jQuery 的 fn 和 prototype 属性指向 fn 对象,而 init 函数本身就是 jQuery.prototype 中的方法,且 init 函数的 prototype 原型指向 fn。

链式调用

链式调用的好处,就是写出来的代码非常简洁,而且代码返回的都是同一个对象,提高代码效率。

前面已经说了,在没有返回值的原型函数后面添加 return this:

var jQuery = function(){
  return new jQuery.fn.init();
}
jQuery.fn = jQuery.prototype = {
  constructor: jQuery,
  init: function(){
    this.jquery = 3.0;
    return this;
  },
  each: function(){
    console.log("each");
    return this;
  }
}
jQuery.fn.init.prototype = jQuery.fn;
jQuery().each().each();
// "each"
// "each"
extend

jQuery 中一个重要的函数便是 extend,既可以对本身 jQuery 的属性和方法进行扩张,又可以对原型的属性和方法进行扩展。

先来说下 extend 函数的功能,大概有两种,如果参数只有一个 object,即表示将这个对象扩展到 jQuery 的命名空间中,也就是所谓的 jQuery 的扩展。如果函数接收了多个 object,则表示一种属性拷贝,将后面多个对象的属性全拷贝到第一个对象上,这其中,还包括深拷贝,即非引用拷贝,第一个参数如果是 true 则表示深拷贝。

jQuery.extend(target);// jQuery 的扩展
jQuery.extend(target, obj1, obj2,..);//浅拷贝 
jQuery.extend(true, target, obj1, obj2,..);//深拷贝 

以下是 jQuery 3 之后的 extend 函数源码,自己做了注释:

jQuery.extend = jQuery.fn.extend = function () {
  var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
    i = 1,
    length = arguments.length,
    deep = false;

  // 判断是否为深拷贝
  if (typeof target === "boolean") {
    deep = target;

    // 参数后移
    target = arguments[i] || {};
    i++;
  }

  // 处理 target 是字符串或奇怪的情况,isFunction(target) 可以判断 target 是否为函数
  if (typeof target !== "object" && !jQuery.isFunction(target)) {
    target = {};
  }

  // 判断是否 jQuery 的扩展
  if (i === length) {
    target = this; // this 做一个标记,可以指向 jQuery,也可以指向 jQuery.fn
    i--;
  }

  for (; i < length; i++) {

    // null/undefined 判断
    if ((options = arguments[i]) != null) {

      // 这里已经统一了,无论前面函数的参数怎样,现在的任务就是 target 是目标对象,options 是被拷贝对象
      for (name in options) {
        src = target[name];
        copy = options[name];

        // 防止死循环,跳过自身情况
        if (target === copy) {
          continue;
        }

        // 深拷贝,且被拷贝对象是 object 或 array
        // 这是深拷贝的重点
        if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))) {
          // 说明被拷贝对象是数组
          if (copyIsArray) {
            copyIsArray = false;
            clone = src && Array.isArray(src) ? src : [];
          // 被拷贝对象是 object
          } else {
            clone = src && jQuery.isPlainObject(src) ? src : {};
          }

          // 递归拷贝子属性
          target[name] = jQuery.extend(deep, clone, copy);

          // 常规变量,直接 =
        } else if (copy !== undefined) {
            target[name] = copy;
        }
      }
    }
  }

  // Return the modified object
  return target;
}

extend 函数符合 jQuery 中的参数处理规范,算是比较标准的一个。jQuery 对于参数的处理很有一套,总是喜欢错位来使得每一个位置上的变量和它们的名字一样,各司其职。比如 target 是目标对象,如果第一个参数是 boolean 型的,就对 deep 赋值 target,并把 target 向后移一位;如果参数对象只有一个,即对 jQuery 的扩展,就令 target 赋值 this,当前指针 i 减一。

这种方法逻辑虽然很复杂,但是带来一个非常大的优势:后面的处理逻辑只需要一个就可以。target 就是我们要拷贝的目标,options 就是要拷贝的对象,逻辑又显得非常的清晰。

extend 函数还需要主要一点,jQuery.extend = jQuery.fn.extend,不仅 jQuery 对象又这个函数,连原型也有,那么如何区分对象是扩展到哪里了呢,又是如何实现的?

其实这一切都要借助与 javascript 中 this 的动态性,target = this,代码就放在那里,谁去执行,this 就会指向谁,就会在它的属性上扩展。

由 extend 衍生的函数

再看 extend 源码,里面有一些函数,只是看名字知道了它是干什么的,我专门挑出来,找到它们的源码。

jQuery.isFunction 源码
jQuery.isFunction = function (obj) {
    return jQuery.type(obj) === "function";
}

这也太简单了些。这里又要引出 jQuery 里一个重要的函数 jQuery.type,这个函数用于类型判断。

首先,为什么传统的 typeof 不用?因为不好用(此处应有一个哭脸):

// Numbers
typeof 37 === "number";
typeof 3.14 === "number";
typeof(42) === "number";
typeof Math.LN2 === "number";
typeof Infinity === "number";
typeof NaN === "number"; // Despite being "Not-A-Number"
typeof Number(1) === "number"; // but never use this form!

// Strings
typeof "" === "string";
typeof "bla" === "string";
typeof (typeof 1) === "string"; // typeof always returns a string
typeof String("abc") === "string"; // but never use this form!

// Booleans
typeof true === "boolean";
typeof false === "boolean";
typeof Boolean(true) === "boolean"; // but never use this form!

// Symbols
typeof Symbol() === "symbol"
typeof Symbol("foo") === "symbol"
typeof Symbol.iterator === "symbol"

// Undefined
typeof undefined === "undefined";
typeof declaredButUndefinedVariable === "undefined";
typeof undeclaredVariable === "undefined"; 

// Objects
typeof {a:1} === "object";

// use Array.isArray or Object.prototype.toString.call
// to differentiate regular objects from arrays
typeof [1, 2, 4] === "object";

typeof new Date() === "object";

// The following is confusing. Don"t use!
typeof new Boolean(true) === "object"; 
typeof new Number(1) === "object"; 
typeof new String("abc") === "object";

// Functions
typeof function(){} === "function";
typeof class C {} === "function";
typeof Math.sin === "function";

// This stands since the beginning of JavaScript
typeof null === "object";

可以看得出来,对于一些 new 对象,比如 new Number(1),也会返回 object。具体请参考typeof MDN。

网上有两种解决方法(有效性未经考证,请相信 jQuery 的方法),一种是用 constructor.nameObject.prototype.constructor MDN,一种是用 Object.prototype.toString.call()Object.prototype.toString(),最终 jQuery 选择了后者。

var n1 = 1;
n1.constructor.name;//"Number"
var n2 = new Number(1);
n2.constructor.name;//"Number"

var toString = Object.prototype.toString;
toString.call(n1);//"[object Number]"
toString.call(n2);//"[object Number]"

以上属于科普,原理不多阐述,接下来继续看源码 jQuery.type

// 这个对象是用来将 toString 函数返回的字符串转成
var class2type = {
    "[object Boolean]": "boolean",
    "[object Number]": "number",
    "[object String]": "string",
    "[object Function]": "function",
    "[object Array]": "array",
    "[object Date]": "date",
    "[object RegExp]": "regexp",
    "[object Object]": "object",
    "[object Error]": "error",
    "[object Symbol]": "symbol"
}
var toString = Object.prototype.toString;

jQuery.type = function (obj) {
    if (obj == null) {
        return obj + "";
    }
    return 
      typeof obj === "object" || typeof obj === "function" ? 
        class2type[toString.call(obj)] || "object" : 
        typeof obj;
}

因为 jQuery 用的是 toString 方法,所以需要有一个 class2type 的对象用来转换。

jQuery.isPlainObject

这个函数用来判断对象是否是一个纯粹的对象,:

var getProto = Object.getPrototypeOf;//获取父对象
var hasOwn = class2type.hasOwnProperty;
var fnToString = hasOwn.toString;
var ObjectFunctionString = fnToString.call( Object );

jQuery.isPlainObject = function (obj) {
    var proto, Ctor;

    // 排除 underfined、null 和非 object 情况
    if (!obj || toString.call(obj) !== "[object Object]") {
        return false;
    }

    proto = getProto(obj);

    // Objects with no prototype (e.g., `Object.create( null )`) are plain
    if (!proto) {
        return true;
    }

    // Objects with prototype are plain iff they were constructed by a global Object function
    Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
    return typeof Ctor === "function" && fnToString.call(Ctor) === ObjectFunctionString;
}

看一下效果:

jQuery.isPlainObject({});// true
jQuery.isPlainObject({ a: 1 });// true
jQuery.isPlainObject(new Object());// true

jQuery.isPlainObject([]);// false
jQuery.isPlainObject(new String("a"));// false
jQuery.isPlainObject(function(){});// false

除了这几个函数之外,还有个 Array.isArray(),这个真的不用介绍了吧。

总结

总结还是多说一点的好,现在已经基本理清 jQuery 内部的情况了?no,还差一点,看下面的代码:

(function(window) {
  // jQuery 变量,用闭包避免环境污染
  var jQuery = (function() {
    var jQuery = function(selector, context) {
        return new jQuery.fn.init(selector, context, rootjQuery);
    };

    // 一些变量声明

    jQuery.fn = jQuery.prototype = {
        constructor: jQuery,
        init: function(selector, context, rootjQuery) {
          // 下章会重点讨论
        }

        // 原型方法
    };

    jQuery.fn.init.prototype = jQuery.fn;

    jQuery.extend = jQuery.fn.extend = function() {};//已介绍

    jQuery.extend({
        // 一堆静态属性和方法
        // 用 extend 绑定,而不是直接在 jQuery 上写
    });

    return jQuery;
  })();

  // 工具方法 Utilities
  // 回调函数列表 Callbacks Object
  // 异步队列 Defferred Object
  // 浏览器功能测试 Support
  // 数据缓存 Data
  // 队列 Queue
  // 属性操作 Attributes
  // 事件系统 Events
  // 选择器 Sizzle
  // DOM遍历 Traversing
  // 样式操作 CSS(计算样式、内联样式)
  // 异步请求 Ajax
  // 动画 Effects
  // 坐标 Offset、尺寸 Dimensions

  window.jQuery = window.$ = jQuery;
})(window);

可以看出 jQuery 很巧妙的整体布局思路,对于属性方法和原型方法等区分,防止变量污染等,都做的非常好。阅读框架源码只是开头,有趣的还在后面。

参考

jQuery 2.0.3 源码分析core - 整体架构
《jQuery源码解析》读书笔记(第二章:构造jQuery对象)
jQuery.isPlainObject() 函数详解

本文在 github 上的源码地址,欢迎来 star。

欢迎来我的博客交流。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/88136.html

相关文章

  • jQuery 源码系列(十)event 总体概述

    摘要:而事件委托的概念事件目标自身不处理事件,而是将其委托给父元素或祖先元素或根元素,而借助事件的冒泡性质由内向外来达到最终处理事件。而且一旦出现,局部刷新导致重新绑定事件。函数的用法,代表要移除的事件,表示选择的,表示事件处理函数。 欢迎来我的专栏查看系列文章。 这次的内容是来介绍关于 jQuery 的事件委托。不过在之前呢有必要先来了解一下 JS 中的事件委托与冒泡,我之前也写过类似的博...

    liujs 评论0 收藏0
  • jQuery 源码系列(二)init 介绍

    摘要:源码中接受个参数,空参数,这个会直接返回一个空的对象,。,这是一个标准且常用法,表示一个选择器,这个选择器通常是一个字符串,或者等,表示选择范围,即限定作用,可为,对象。,会把普通的对象或对象包装在对象中。介绍完入口,就开始来看源码。 欢迎来我的专栏查看系列文章。 init 构造器 前面一讲总体架构已经介绍了 jQuery 的基本情况,这一章主要来介绍 jQuery 的入口函数 jQu...

    Tony_Zby 评论0 收藏0
  • Underscore 源码总体架构

    摘要:不过这样子又回带来另一个问题,对于函数,函数返回什么不重要,主要是处理过程,可以支持链式调用,对于函数,返回的是处理后的结果,可以不用链式,所以函数就是来判断是否需要链式,而对返回值进行处理。然后后面还有一个函数,也是用来作为回调函数的。 其实,学习一个库的源码,最重要的就是先理清它的基本架构,jQuery 是这样,Underscore 也应该是这样。 Underscore 这个库提供...

    zhunjiee 评论0 收藏0
  • 前端经验 - 收藏集 - 掘金

    摘要:我拖拖拖拖放基础篇前端掘金不要搞错,本文不是讲如何拖地的。结构说明前端应该从哪些方面来优化网站前端掘金不知道是哪位大牛的文章,转过来回答。 我拖拖拖 --H5 拖放 API 基础篇 - 前端 - 掘金不要搞错,本文不是讲如何拖地的。看过《javascript精粹》朋友应该知道,他实现拖放的过程比较复杂,现在时代不同了,我们用H5的新的拖放API就能非常方便的实现拖放效果了。最近在园子见...

    MudOnTire 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<