学习 jQuery 源码整体架构,打造属于自己的 js 类库

若川大约 12 分钟

学习 jQuery 源码整体架构,打造属于自己的 js 类库

1. 前言

写于2019年7月29日

大家好,我是若川open in new window。我倾力持续组织了一年每周大家一起学习200行左右的源码共读活动open in new window,感兴趣的可以点此扫码加我微信 ruochuan02 参与open in new window。另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》open in new window,目前是掘金关注人数(4.1k+人)第一的专栏,写有20余篇源码文章。

这是学习源码整体架构系列第一篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。本篇文章学习的是打包整合后的代码,不是实际仓库中的拆分的代码。

本文仓库地址open in new windowgit clone https://github.com/lxchuan12/jquery-analysis.git

要是有人说到怎么读源码,正在读文章的你能推荐我的源码系列文章,那真是太好了

学习源码整体架构系列文章如下:

1.学习 jQuery 源码整体架构,打造属于自己的 js 类库open in new window
2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库open in new window
3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库open in new window
4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDKopen in new window
5.学习 vuex 源码整体架构,打造属于自己的状态管理库open in new window
6.学习 axios 源码整体架构,打造属于自己的请求库open in new window
7.学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理open in new window
8.学习 redux 源码整体架构,深入理解 redux 及其中间件原理open in new window

感兴趣的读者可以点击阅读。
其他源码计划中的有:expressopen in new windowvue-rotueropen in new windowreact-reduxopen in new window 等源码,不知何时能写完(哭泣),欢迎持续关注我(若川)。

源码类文章,一般阅读量不高。已经有能力看懂的,自己就看了。不想看,不敢看的就不会去看源码。
所以我的文章,尽量写得让想看源码又不知道怎么看的读者能看懂。

虽然现在基本不怎么使用jQuery了,但jQuery流行10多年JS库,还是有必要学习它的源码的。也可以学着打造属于自己的js类库,求职面试时可以增色不少。

本文章学习的是v3.4.1 版本。 unpkg.com jQuery源码地址open in new window

jQuery github仓库open in new window

2. 自执行匿名函数

(function(global, factory){

})(typeof window !== "underfined" ? window: this, function(window, noGlobal){

});

外界访问不到里面的变量和函数,里面可以访问到外界的变量,但里面定义了自己的变量,则不会访问外界的变量。 匿名函数将代码包裹在里面,防止与其他代码冲突和污染全局环境。 关于自执行函数不是很了解的读者可以参看这篇文章。 [译] JavaScript:立即执行函数表达式(IIFE)open in new window

浏览器环境下,最后把$jQuery函数挂载到window上,所以在外界就可以访问到$jQuery了。

if ( !noGlobal ) {
	window.jQuery = window.$ = jQuery;
}
// 其中`noGlobal`参数只有在这里用到。

3. 支持多种环境下使用 比如 commonjs、amd规范

3.1 commonjs 规范支持

commonjs实现 主要代表 nodejs

// global是全局变量,factory 是函数
( function( global, factory ) {

	//  使用严格模式
	"use strict";
	// Commonjs 或者 CommonJS-like  环境
	if ( typeof module === "object" && typeof module.exports === "object" ) {
		// 如果存在global.document 则返回factory(global, true);
		module.exports = global.document ?
			factory( global, true ) :
			function( w ) {
				if ( !w.document ) {
					throw new Error( "jQuery requires a window with a document" );
				}
				return factory( w );
			};
	} else {
		factory( global );
	}

// Pass this if window is not defined yet
// 第一个参数判断window,存在返回window,不存在返回this
} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {});

3.2 amd 规范 主要代表 requirejs

if ( typeof define === "function" && define.amd ) {
	define( "jquery", [], function() {
		return jQuery;
	} );
}

3.3 cmd 规范 主要代表 seajs

很遗憾,jQuery源码里没有暴露对seajs的支持。但网上也有一些方案。这里就不具体提了。毕竟现在基本不用seajs了。

4. 无 new 构造

实际上也是可以 new的,因为jQuery是函数。而且和不用new效果是一样的。 new显示返回对象,所以和直接调用jQuery函数作用效果是一样的。 如果对new操作符具体做了什么不明白。可以参看我之前写的文章。

面试官问:能否模拟实现JS的new操作符open in new window

源码:

 var
	version = "3.4.1",

	// Define a local copy of jQuery
	jQuery = function( selector, context ) {
		// 返回new之后的对象
		return new jQuery.fn.init( selector, context );
	};
jQuery.fn = jQuery.prototype = {
	// jQuery当前版本
	jquery: version,
	// 修正构造器为jQuery
	constructor: jQuery,
	length: 0,
};
init = jQuery.fn.init = function( selector, context, root ) {
	// ...
	if ( !selector ) {
		return this;
	}
	// ...
};
init.prototype = jQuery.fn;
jQuery.fn === jQuery.prototype; 	// true
init = jQuery.fn.init;
init.prototype = jQuery.fn;
// 也就是
jQuery.fn.init.prototype === jQuery.fn;	 // true
jQuery.fn.init.prototype === jQuery.prototype; 	// true

关于这个笔者画了一张jQuery原型关系图,所谓一图胜千言。 jQuery-v3.4.1原型关系图

<sciprt src="https://unpkg.com/jquery@3.4.1/dist/jquery.js">
</script>
console.log({jQuery});
// 在谷歌浏览器控制台,可以看到jQuery函数下挂载了很多静态属性和方法,在jQuery.fn 上也挂着很多属性和方法。

Vue源码中,也跟jQuery类似,执行的是Vue.prototype._init方法。

function Vue (options) {
	if (!(this instanceof Vue)
	) {
		warn('Vue is a constructor and should be called with the `new` keyword');
	}
	this._init(options);
}
initMixin(Vue);
function initMixin (Vue) {
	Vue.prototype._init = function (options) {};
};

5. 核心函数之一 extend

用法:

jQuery.extend( target [, object1 ] [, objectN ] )        Returns: Object

jQuery.extend( [deep ], target, object1 [, objectN ] )

jQuery.extend APIopen in new windowjQuery.fn.extend APIopen in new window

看几个例子: (例子可以我放到在线编辑代码的jQuery.extend例子codepenopen in new window了,可以直接运行)。

// 1. jQuery.extend( target)
var result1 = $.extend({
	job: '前端开发工程师',
});

console.log(result1, 'result1', result1.job); // $函数 加了一个属性 job  // 前端开发工程师

// 2. jQuery.extend( target, object1)
var result2 = $.extend({
	name: '若川',
},
{
	job: '前端开发工程师',
});

console.log(result2, 'result2'); // { name: '若川', job: '前端开发工程师' }

// deep 深拷贝
// 3. jQuery.extend( [deep ], target, object1 [, objectN ] )
var result3 = $.extend(true,  {
	name: '若川',
	other: {
		mac: 0,
		ubuntu: 1,
		windows: 1,
	},
}, {
	job: '前端开发工程师',
	other: {
		mac: 1,
		linux: 1,
		windows: 0,
	}
});
console.log(result3, 'result3');
// deep true
// {
//     "name": "若川",
//     "other": {
//         "mac": 1,
//         "ubuntu": 1,
//         "windows": 0,
//         "linux": 1
//     },
//     "job": "前端开发工程师"
// }
// deep false
// {
//     "name": "若川",
//     "other": {
//         "mac": 1,
//         "linux": 1,
//         "windows": 0
//     },
//     "job": "前端开发工程师"
// }

结论:extend函数既可以实现给jQuery函数可以实现浅拷贝、也可以实现深拷贝。可以给jQuery上添加静态方法和属性,也可以像jQuery.fn(也就是jQuery.prototype)上添加属性和方法,这个功能归功于thisjQuery.extend调用时this指向是jQueryjQuery.fn.extend调用时this指向则是jQuery.fn

5.1 浅拷贝实现

知道这些,其实实现浅拷贝还是比较容易的:

// 浅拷贝实现
jQuery.extend = function(){
	// options 是扩展的对象object1,object2...
	var options,
	// object对象上的键
	name,
	// copy object对象上的值,也就是是需要拷贝的值
	copy,
	// 扩展目标对象,可能不是对象,所以或空对象
	target = arguments[0] || {},
	// 定义i为1
	i = 1,
	// 定义实参个数length
	length = arguments.length;
	// 只有一个参数时
	if(i === length){
		target = this;
		i--;
	}
	for(; i < length; i++){
		// 不是underfined 也不是null
		if((options = arguments[i]) !=  null){
			for(name in options){
				copy = options[name];
				// 防止死循环,continue 跳出当前此次循环
				if ( name === "__proto__" || target === copy ) {
					continue;
				}
				if ( copy !== undefined ) {
					target[ name ] = copy;
				}
			}
		}

	}
	// 最后返回目标对象
	return target;
}

深拷贝则主要是在以下这段代码做判断。可能是数组和对象引用类型的值,做判断。

if ( copy !== undefined ) {
	target[ name ] = copy;
}

为了方便读者调试,代码同样放在jQuery.extend浅拷贝代码实现codepenopen in new window,可在线运行。

5.2 深拷贝实现

$.extend = function(){
	// options 是扩展的对象object1,object2...
	var options,
	// object对象上的键
	name,
	// copy object对象上的值,也就是是需要拷贝的值
	copy,
	// 深拷贝新增的四个变量 deep、src、copyIsArray、clone
	deep = false,
	// 源目标,需要往上面赋值的
	src,
	// 需要拷贝的值的类型是函数
	copyIsArray,
	//
	clone,
	// 扩展目标对象,可能不是对象,所以或空对象
	target = arguments[0] || {},
	// 定义i为1
	i = 1,
	// 定义实参个数length
	length = arguments.length;

	// 处理深拷贝情况
	if ( typeof target === "boolean" ) {
		deep = target;

		// Skip the boolean and the target
		// target目标对象开始后移
		target = arguments[ i ] || {};
		i++;
	}

	// Handle case when target is a string or something (possible in deep copy)
	// target不等于对象,且target不是函数的情况下,强制将其赋值为空对象。
	if ( typeof target !== "object" && !isFunction( target ) ) {
		target = {};
	}

	// 只有一个参数时
	if(i === length){
		target = this;
		i--;
	}
	for(; i < length; i++){
		// 不是underfined 也不是null
		if((options = arguments[i]) !=  null){
			for(name in options){
				copy = options[name];
				// 防止死循环,continue 跳出当前此次循环
				if ( name === "__proto__" || target === copy ) {
					continue;
				}

				// Recurse if we're merging plain objects or arrays
				// 这里deep为true,并且需要拷贝的值有值,并且是纯粹的对象
				// 或者需拷贝的值是数组
				if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
					( copyIsArray = Array.isArray( copy ) ) ) ) {

					// 源目标,需要往上面赋值的
					src = target[ name ];

					// Ensure proper type for the source value
					// 拷贝的值,并且src不是数组,clone对象改为空数组。
					if ( copyIsArray && !Array.isArray( src ) ) {
						clone = [];
						// 拷贝的值不是数组,对象不是纯粹的对象。
					} else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {
						// clone 赋值为空对象
						clone = {};
					} else {
						// 否则 clone = src
						clone = src;
					}
					// 把下一次循环时,copyIsArray 需要重新赋值为false
					copyIsArray = false;

					// 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 target;
};

为了方便读者调试,这段代码同样放在jQuery.extend深拷贝代码实现codepenopen in new window,可在线运行。

5.3 深拷贝衍生的函数 isFunction

判断参数是否是函数。

var isFunction = function isFunction( obj ) {

	// Support: Chrome <=57, Firefox <=52
	// In some browsers, typeof returns "function" for HTML <object> elements
	// (i.e., `typeof document.createElement( "object" ) === "function"`).
	// We don't want to classify *any* DOM node as a function.
	return typeof obj === "function" && typeof obj.nodeType !== "number";
};

5.4 深拷贝衍生的函数 jQuery.isPlainObject

jQuery.isPlainObject(obj) 测试对象是否是纯粹的对象(通过 "{}" 或者 "new Object" 创建的)。

jQuery.isPlainObject({}) // true
jQuery.isPlainObject("test") // false
var getProto = Object.getPrototypeOf;
var class2type = {};
var toString = class2type.toString;
var hasOwn = class2type.hasOwnProperty;
var fnToString = hasOwn.toString;
var ObjectFunctionString = fnToString.call( Object );

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

		// Detect obvious negatives
		// Use toString instead of jQuery.type to catch host objects
		// !obj 为true或者 不为[object Object]
		// 直接返回false
		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) 直接返回 true;
		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;
		// 构造器是函数,并且 fnToString.call( Ctor )  === fnToString.call( Object );
		return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
	},
});

extend函数,也可以自己删掉写一写,算是jQuery中一个比较核心的函数了。而且用途广泛,可以内部使用也可以,外部使用扩展 插件等。

6. 链式调用

jQuery能够链式调用是因为一些函数执行结束后 return this。 比如 jQuery 源码中的addClassremoveClasstoggleClass

jQuery.fn.extend({
	addClass: function(){
		// ...
		return this;
	},
	removeClass: function(){
		// ...
		return this;
	},
	toggleClass: function(){
		// ...
		return this;
	},
});

7. jQuery.noConflict 很多js库都会有的防冲突函数

jQuery.noConflict APIopen in new window

用法:

 <script>
	var $ = '我是其他的$,jQuery不要覆盖我';
</script>
<script src="./jquery-3.4.1.js">
</script>
<script>
	$.noConflict();
	console.log($); // 我是其他的$,jQuery不要覆盖我
</script>

jQuery.noConflict 源码

var

	// Map over jQuery in case of overwrite
	_jQuery = window.jQuery,

	// Map over the $ in case of overwrite
	_$ = window.$;

jQuery.noConflict = function( deep ) {
	// 如果已经存在$ === jQuery;
	// 把已存在的_$赋值给window.$;
	if ( window.$ === jQuery ) {
		window.$ = _$;
	}

	// 如果deep为 true, 并且已经存在jQuery === jQuery;
	// 把已存在的_jQuery赋值给window.jQuery;
	if ( deep && window.jQuery === jQuery ) {
		window.jQuery = _jQuery;
	}

	// 最后返回jQuery
	return jQuery;
};

8. 总结

全文主要通过浅析了jQuery整体结构,自执行匿名函数、无new构造、支持多种规范(如commonjs、amd规范)、核心函数之extend、链式调用、jQuery.noConflict等方面。

重新梳理下文中学习的源码结构。

// 源码结构
( function( global, factory )
	"use strict";
	if ( typeof module === "object" && typeof module.exports === "object" ) {
		module.exports = global.document ?
			factory( global, true ) :
			function( w ) {
				if ( !w.document ) {
					throw new Error( "jQuery requires a window with a document" );
				}
				return factory( w );
			};
	} else {
		factory( global );
	}

} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
	var	version = "3.4.1",

		// Define a local copy of jQuery
		jQuery = function( selector, context ) {
			return new jQuery.fn.init( selector, context );
		};

	jQuery.fn = jQuery.prototype = {
		jquery: version,
		constructor: jQuery,
		length: 0,
		// ...
	};

	jQuery.extend = jQuery.fn.extend = function() {};

	jQuery.extend( {
		// ...
		isPlainObject: function( obj ) {},
		// ...
	});

	init = jQuery.fn.init = function( selector, context, root ) {};

	init.prototype = jQuery.fn;

	if ( typeof define === "function" && define.amd ) {
		define( "jquery", [], function() {
			return jQuery;
		} );
	}
	jQuery.noConflict = function( deep ) {};

	if ( !noGlobal ) {
		window.jQuery = window.$ = jQuery;
	}

	return jQuery;
});

可以学习到jQuery巧妙的设计和架构,为自己所用,打造属于自己的js类库。 相关代码和资源放置在github blogopen in new window中,需要的读者可以自取。

下一篇文章可能是学习underscorejs的源码整体架构。

读者发现有不妥或可改善之处,欢迎评论指出。另外觉得写得不错,可以点赞、评论、转发,也是对笔者的一种支持。

笔者往期文章

面试官问:JS的继承open in new window
面试官问:JS的this指向open in new window
面试官问:能否模拟实现JS的call和apply方法open in new window
面试官问:能否模拟实现JS的bind方法open in new window
面试官问:能否模拟实现JS的new操作符open in new window
前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并open in new window

扩展阅读

chokcoco: jQuery- v1.10.2 源码解读open in new window
chokcoco:【深入浅出jQuery】源码浅析--整体架构open in new window
songjz :jQuery 源码系列(一)总体架构open in new window

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
若川的博客open in new window,使用vuepress重构了,阅读体验可能更好些
掘金专栏open in new window,欢迎关注~
segmentfault前端视野专栏open in new window,开通了前端视野专栏,欢迎关注~
知乎前端视野专栏open in new window,开通了前端视野专栏,欢迎关注~
语雀前端视野专栏open in new window,新增语雀专栏,欢迎关注~
github blogopen in new window,相关源码和资源都放在这里,求个star_~

微信公众号 若川视野

可能比较有趣的微信公众号,长按扫码关注(回复pdf获取前端优质书籍pdf)。欢迎加笔者微信ruochuan12(注明来源,基本来者不拒),拉您进【前端视野交流群】,长期交流学习~

若川视野
若川视野
欢迎扫码加我微信
拉你进源码共读群
一起学习源码