學習 jQuery 源碼總體架構,打造屬於本身的 js 類庫

雖然如今基本不怎麼使用jQuery了,但jQuery流行10多年JS庫,仍是有必要學習它的源碼的。也能夠學着打造屬於本身的js類庫,求職面試時能夠增色很多。html

本文章學習的是v3.4.1版本。 unpkg.com源碼地址:unpkg.com/jquery@3.4.…前端

jQuery github倉庫node

自執行匿名函數

(function(global, factory){

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

});
複製代碼

外界訪問不到裏面的變量和函數,裏面能夠訪問到外界的變量,但裏面定義了本身的變量,則不會訪問外界的變量。 匿名函數將代碼包裹在裏面,防止與其餘代碼衝突和污染全局環境。 關於自執行函數不是很瞭解的讀者能夠參看這篇文章。 [譯] JavaScript:當即執行函數表達式(IIFE)jquery

瀏覽器環境下,最後把$jQuery函數掛載到window上,因此在外界就能夠訪問到$jQuery了。linux

if ( !noGlobal ) {
	window.jQuery = window.$ = jQuery;
}
// 其中`noGlobal`參數只有在這裏用到。
複製代碼

支持多種環境下使用 好比 commonjs、amd規範

commonjs 規範支持

commonjs實現 主要表明 nodejsgit

// 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了。github

無 new 構造

實際上也是能夠 new的,由於jQuery是函數。並且和不用new效果是同樣的。 new顯示返回對象,因此和直接調用jQuery函數做用效果是同樣的。 若是對new操做符具體作了什麼不明白。能夠參看我以前寫的文章。面試

面試官問:可否模擬實現JS的new操做符編程

源碼:ubuntu

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) {};
};
複製代碼

核心函數之一 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的源碼總體架構。學習 underscore 源碼總體架構,打造屬於本身的函數式編程類庫

讀者發現有不妥或可改善之處,歡迎評論指出。另外以爲寫得不錯,能夠點贊、評論、轉發,也是對筆者的一種支持。

筆者往期文章

面試官問:JS的繼承

面試官問:JS的this指向

面試官問:可否模擬實現JS的call和apply方法

面試官問:可否模擬實現JS的bind方法

面試官問:可否模擬實現JS的new操做符

前端使用puppeteer 爬蟲生成《React.js 小書》PDF併合並

擴展閱讀

chokcoco: jQuery- v1.10.2 源碼解讀

chokcoco:【深刻淺出jQuery】源碼淺析--總體架構

songjz :jQuery 源碼系列(一)整體架構

關於

做者:常以若川爲名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,惟善學。
我的博客 https://lxchuan12.github.io
github blog,相關源碼和資源都放在這裏,求個star^_^~

微信公衆號 若川視野

可能比較有趣的微信公衆號,長按掃碼關注。也能夠加微信 lxchuan12,註明來源,拉您進【前端視野交流羣】。

微信公衆號  若川視野
相關文章
相關標籤/搜索