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

欢迎来我的专栏检察系列文章。

决议你走多远的是基础,jQuery 源码剖析,向父老敬拜!

我虽然打仗 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 运用上的几大特性:

  1. $(‘#id’) 函数体式格局直接天生 jQuery 对象

  2. $(‘#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 好写多了吗。

借用网上的一张图:

《jQuery 源码系列(一)整体架构》

从这张图中能够看出,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。

欢迎来我的博客交换。

    原文作者:songjz
    原文地址: https://segmentfault.com/a/1190000008365621
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞