Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ruochuan12 opened this issue Jul 29, 2019 · 0 comments
Open

Comments

@ruochuan12
Copy link
Owner

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

本文章学习的是v3.4.1 版本。
unpkg.com源码地址:https://unpkg.com/[email protected]/dist/jquery.js

jQuery github仓库

自执行匿名函数

(function(global, factory){

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

});

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

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

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

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

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 ) {});

amd 规范 主要代表 requirejs

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

cmd 规范 主要代表 seajs

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

无 new 构造

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

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

源码:

 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/[email protected]/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) {};
};

核心函数之一 extend

用法:

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

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

jQuery.extend API
jQuery.fn.extend API

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

// 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

浅拷贝实现

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

// 浅拷贝实现
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浅拷贝代码实现codepen,可在线运行。

深拷贝实现

$.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深拷贝代码实现codepen,可在线运行。

深拷贝衍生的函数 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";
};

深拷贝衍生的函数 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中一个比较核心的函数了。而且用途广泛,可以内部使用也可以,外部使用扩展 插件等。

链式调用

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

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

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

jQuery.noConflict API

用法:

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

总结

全文主要通过浅析了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 blog中,需要的读者可以自取。

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

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

笔者往期文章

面试官问:JS的继承

面试官问:JS的this指向

面试官问:能否模拟实现JS的call和apply方法

面试官问:能否模拟实现JS的bind方法

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

前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并

扩展阅读

chokcoco: jQuery- v1.10.2 源码解读

chokcoco:【深入浅出jQuery】源码浅析--整体架构

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

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。

个人博客

掘金专栏,欢迎关注~

segmentfault前端视野专栏,开通了前端视野专栏,欢迎关注~

知乎前端视野专栏,开通了前端视野专栏,欢迎关注~

github blog,相关源码和资源都放在这里,求个star^_^~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant