jQuery源码分析(一):Core.js部分

总体架构

Core即核心,jQuery的核心部分主要做了这么几件事

  1. 通过require.js 加载了var中的各个模块,获取了关于数组和对象的各种方法,比如slice, concat, push等,这样做使得jQuery对象的各类方法解耦,在模块中提供统一的接口,抹平浏览器之间的差异
  2. 定义一个jQuery函数并作为模块最后的返回值,这个jQuery函数调用时会调用jQuery.fn.init(这个init函数会在/core/init.js中实现)
  3. 添加一些数组和对象方法绑定到jQueryprototype上。其中如map, forEach等方法会调用jQuery实例上的一些方法。这些实例上的方法会在第五条通过在后面的代码中调用jQuery.extend来添加
  4. 定义jQuery.extend, 主要是为了方便外部使用jQuery.extend来扩展jQuery,至于合并多个对象,这个功能是送的
  5. 调用jQuery.extend,将require.js加载的数组方法和对象添加到jQuery的实例上

代码部分

/* global Symbol */
// Defining this global in .eslintrc.json would create a danger of using the global
// unguarded in another place, it seems safer to define global only for this module

define([
	"./var/arr", // 对js原生数组的引用
	"./var/document", // 对document对象的引用
	"./var/getProto", // 对Object.getPrototypeOf的引用
	"./var/slice", // 对Array.prototype.slice的引用
	"./var/concat", // 对Array.prototype.concat的引用
	"./var/push", // 对Array.prototype.push的引用
	"./var/indexOf", // 对Array.prototype.indexOf的引用
	"./var/class2type", // 一个对象(字典),里面有类到类名的映射
	"./var/toString" // Object.prototype.toString的引用
	"./var/hasOwn", // Object.prototype.hasOwnProperty的引用
	"./var/fnToString", // Function.prototype.toString的引用
	"./var/ObjectFunctionString", // Object构造函数调用toString()的结果。即`Object.toString()`
	"./var/support", // 记录浏览器是否支持某种特性的一个对象,jQuery内部没有用到这个变量,但可能会有扩展jQuery的库需要他
	"./var/isFunction", // 判断是否是函数(某些浏览器中会出现`typeof document.createElement( "object" ) === "function`)的情况,因此做了兼容
	"./var/isWindow", // 判断是否是window对象
	"./core/DOMEval", // 这个`eval`的实现是把代码挂载到`<script>`标签中执行后删除`script`标签
	"./core/toType" // 获得对象的类型
], function (arr, document, getProto, slice, concat, push, indexOf,
	class2type, toString, hasOwn, fnToString, ObjectFunctionString,
	support, isFunction, isWindow, DOMEval, toType) {

		"use strict";

		var
			version = "3.3.1",

			// Define a local copy of jQuery
			// 定义jQuery函数
			jQuery = function (selector, context) {

				// The jQuery object is actually just the init constructor 'enhanced'
				// Need init if jQuery is called (just allow error to be thrown if not included)
				return new jQuery.fn.init(selector, context);
			},

			// Support: Android <=4.0 only
			// Make sure we trim BOM and NBSP
			// 移除空格需要的正则表达式
			rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;

		jQuery.fn = jQuery.prototype = {

			// The current version of jQuery being used
			jquery: version,

			// 构造函数
			constructor: jQuery,

			// The default length of a jQuery object is 0
			length: 0,

      // 把jQuery选择到的元素转化成数组
			toArray: function () {
				return slice.call(this);
			},

			// Get the Nth element in the matched element set OR
      // Get the whole matched element set as a clean array
      // 按照索引获取`jQuery`选择到的元素
			get: function (num) {

				// Return all the elements in a clean array
				if (num == null) {
					return slice.call(this);
				}

				// Return just the one element from the set
				return num < 0 ? this[num + this.length] : this[num];
			},

			// Take an array of elements and push it onto the stack
      // (returning the new matched element set)
      // 返回一个新的元素加入到`jQuery`选择器选出的数组中的jQuery实例,他的`prevObject`指向merge之前的实例
			pushStack: function (elems) {

				// Build a new jQuery matched element set
				// 此处调用了`jQuery.merge`合并两个类数组的对象
				var ret = jQuery.merge(this.constructor(), elems);

				// Add the old object onto the stack (as a reference)
				ret.prevObject = this;

				// Return the newly-formed element set
				return ret;
			},

      // Execute a callback for every element in the matched set.
      // each方法,会在下面通过`jQuery.extend`被加入到jQuery的实例上
			each: function (callback) {
				return jQuery.each(this, callback);
			},

      // 通过调用上面的pushStack方法将map后的数组重新组成一个jQUery的对象
			map: function (callback) {
				return this.pushStack(jQuery.map(this, function (elem, i) {
					return callback.call(elem, i, elem);
				}));
			},

      // 同上
			slice: function () {
				return this.pushStack(slice.apply(this, arguments));
			},

      // 返回第一个元素
			first: function () {
				return this.eq(0);
			},
      // 返回最后一个元素
			last: function () {
				return this.eq(-1);
			},

      // 返回某个index的元素,index为负数就从末尾往前找
			eq: function (i) {
				var len = this.length,
					j = +i + (i < 0 ? len : 0);
				return this.pushStack(j >= 0 && j < len ? [this[j]] : []);
			},

      // 结束当前链条最近的筛选操作
			end: function () {
				return this.prevObject || this.constructor();
			},

			// For internal use only.
      // Behaves like an Array's method, not like a jQuery method.
      // 数组方法
			push: push,
			sort: arr.sort,
			splice: arr.splice
		};

		jQuery.extend = jQuery.fn.extend = function () {
			var options, name, src, copy, copyIsArray, clone,
				target = arguments[0] || {},
				// 正常情况下从为source `arguments[1]`到`arguments[length - 1]`
				i = 1,
				length = arguments.length,
				deep = false;

			// Handle a deep copy situation
			if (typeof target === "boolean") {
				deep = target;

				// Skip the boolean and the target
				// 这种情况下从arguments[2]到arguments[length -1]为source
				target = arguments[i] || {};
				i++;
			}

      // Handle case when target is a string or something (possible in deep copy)
      // 因为有那种类Object的Function,即绑定了字段的Function所以要排除这种情况
			if (typeof target !== "object" && !isFunction(target)) {
				target = {};
			}

      // Extend jQuery itself if only one argument is passed
      // 第一个参数不为boolean
			if (i === length) {
				target = this;
				// 这种情况下为arguments[0]到argument[length - 1]为source,this为target
				i--;
			}

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

				// Only deal with non-null/undefined values
				if ((options = arguments[i]) != null) {

					// Extend the base object
					// 如果传入的是数组,name就为Number
					for (name in options) {
						src = target[name]; // Target的[name]字段
						copy = options[name]; // source的[name]字段

						// Prevent never-ending loop
						// 两者相等直接跳过
						if (target === copy) {
							continue;
						}

						// Recurse if we're merging plain objects or arrays
						// 如果是深拷贝且这个字段非nullish 且需要拷贝的字段类型是数组或plain object
						if (deep && copy && (jQuery.isPlainObject(copy) ||
							(copyIsArray = Array.isArray(copy)))) {

							if (copyIsArray) { // 需要拷贝的字段是数组
								copyIsArray = false;
								clone = src && Array.isArray(src) ? src : [];

							} else { // 需要拷贝的字段是对象
								clone = src && jQuery.isPlainObject(src) ? src : {};
							}

							// Never move original objects, clone them
							// 递归深拷贝这个数组或plain object
							target[name] = jQuery.extend(deep, clone, copy);

							// Don't bring in undefined values
						} else if (copy !== undefined) { // 浅拷贝,过滤掉undefined的值
							target[name] = copy;
						}
					}
				}
			}

			// Return the modified object
			return target;
		};

		jQuery.extend({

			// Unique for each copy of jQuery on the page
			expando: "jQuery" + (version + Math.random()).replace(/\D/g, ""),

			// Assume jQuery is ready without the ready module
			// jQuery的所有module是否都加载完毕的标志
			isReady: true,
			
		
			error: function (msg) {
				throw new Error(msg);
			},

			// 空函数
			noop: function () { },

			// 判断是否是Plain object
			isPlainObject: function (obj) {
				var proto, Ctor;

				// Detect obvious negatives
				// Use toString instead of jQuery.type to catch host objects
				// 所有的PlainObject调用toString后都会返回`[object, object]`
				if (!obj || toString.call(obj) !== "[object Object]") {
					return false;
				}

				proto = getProto(obj);

				// Objects with no prototype (e.g., `Object.create( null )`) are plain
				// `Object.create(null)`返回的对象没有prototype
				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;
				// 通过比较构造函数的toString方法来看obj的原型是否是Object
				return typeof Ctor === "function" && fnToString.call(Ctor) === ObjectFunctionString;
			},

			// 判断是否为空对象
			isEmptyObject: function (obj) {

				/* eslint-disable no-unused-vars */
				// See https://github.com/eslint/eslint/issues/6125
				var name;

				for (name in obj) {
					return false;
				}
				return true;
			},

			// DOMEval的引用拷贝
			// Evaluates a script in a global context
			globalEval: function (code) {
				DOMEval(code);
			},

			// array.each函数绑定在jQuery对象上,上面的prototype通过调用它并传入this来实现`$('foo').each`
			each: function (obj, callback) {
				var length, i = 0;

				if (isArrayLike(obj)) {// 类数组
					length = obj.length;
					for (; i < length; i++) {
						// jQuery.each的callback的this指向当前循环的对象
						if (callback.call(obj[i], i, obj[i]) === false) {
							break;
						}
					}
				} else { // 相当于obj.values()
					for (i in obj) {
						if (callback.call(obj[i], i, obj[i]) === false) {
							break;
						}
					}
				}

				return obj;
			},

			// Support: Android <=4.0 only
			// 兼容安卓4.0的trim
			trim: function (text) {
				return text == null ?
					"" :
					(text + "").replace(rtrim, "");
			},

			// results is for internal usage only
			makeArray: function (arr, results) {
				var ret = results || [];

				if (arr != null) {
					if (isArrayLike(Object(arr))) {
						jQuery.merge(ret,
							typeof arr === "string" ?
								[arr] : arr
						);
					} else {
						push.call(ret, arr);
					}
				}

				return ret;
			},

			inArray: function (elem, arr, i) {
				return arr == null ? -1 : indexOf.call(arr, elem, i);
			},

			// Support: Android <=4.0 only, PhantomJS 1 only
			// push.apply(_, arraylike) throws on ancient WebKit
			// 合并2个数组
			merge: function (first, second) {
				var len = +second.length,
					j = 0,
					i = first.length;

				for (; j < len; j++) {
					first[i++] = second[j];
				}

				first.length = i;

				return first;
			},

			// 过滤数组
			grep: function (elems, callback, invert) {
				var callbackInverse,
					matches = [],
					i = 0,
					length = elems.length,
					callbackExpect = !invert;

				// Go through the array, only saving the items
				// that pass the validator function
				for (; i < length; i++) {
					callbackInverse = !callback(elems[i], i);
					// 若`invert === false,`callbackExcept`true`callback`返回`true`的元素会被加入到数组
					// 若`invert === true`callbackExcept`为`false`,`callback`返回`false`的元素会被加入到数组
					if (callbackInverse !== callbackExpect) {
						matches.push(elems[i]);
					}
				}

				return matches;
			},

			// arg is for internal usage only
			// map方法,`jQuery.prototype.map`通过调用这个来实现map
			map: function (elems, callback, arg) {
				var length, value,
					i = 0,
					ret = [];

				// Go through the array, translating each of the items to their new values
				if (isArrayLike(elems)) {
					length = elems.length;
					for (; i < length; i++) {
						value = callback(elems[i], i, arg);

						if (value != null) {
							ret.push(value);
						}
					}

					// Go through every key on the object,
					// 对象`map`的情况,`i`为对象的k`ey`
				} else {
					for (i in elems) {
						value = callback(elems[i], i, arg);

						if (value != null) {
							ret.push(value);
						}
					}
				}

				// Flatten any nested arrays
				return concat.apply([], ret);
			},

			// A global GUID counter for objects
			// 全局`guid`代表`jQuery`对象
			guid: 1,

			// jQuery.support is not used in Core but other projects attach their
			// properties to it so it needs to exist.
			support: support
		});

		// 如果有es6的`Symbol`将`Symbol`部署到`jQuery.fn`上。这样就可以使用for和数组扩展等符号
		if (typeof Symbol === "function") {
			jQuery.fn[Symbol.iterator] = arr[Symbol.iterator];
		}

		// Populate the class2type map
		// 填充class2Type 的字典
		jQuery.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),
			function (i, name) {
				// `Object.prototype.toString.call(foo)`会返回`[object ${typeof foo}]`
				// toType中通过调用`Object.prototype.toString.call`来获得type的key
				class2type["[object " + name + "]"] = name.toLowerCase();
			});

		// 判断是否是类数组的对象
		function isArrayLike(obj) {

			// Support: real iOS 8.2 only (not reproducible in simulator)
			// `in` check used to prevent JIT error (gh-2145)
			// hasOwn isn't used here due to false negatives
			// regarding Nodelist length in IE
			// 使用`in`检查属性是否存在来避免iOS8.2的JIT错误
			// IE下host object(`window`,` Nodelist`, `location`)等没有hasOwnProperty方法
			var length = !!obj && "length" in obj && obj.length,
				type = toType(obj);

			if (isFunction(obj) || isWindow(obj)) {
				return false;
			}

			return type === "array" || length === 0 ||
				typeof length === "number" && length > 0 && (length - 1) in obj;
		}

		return jQuery;
	});

参考资料

jQuery github repo

jQuery API documentation