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

最近一直在研讀 jQuery 源碼,初看源碼一頭霧水毫無頭緒,真正靜下心來細看寫的真是精妙,讓你感嘆代碼之美。javascript

其結構明晰,高內聚、低耦合,兼具優秀的性能與便利的擴展性,在瀏覽器的兼容性(功能缺陷、漸進加強)優雅的處理能力以及 Ajax 等方面周到而強大的定製功能無不使人驚歎。css

另外,閱讀源碼讓我接觸到了大量底層的知識。對原生JS 、框架設計、代碼優化有了全新的認識,接下來將會寫一系列關於 jQuery 解析的文章。html

我在 github 上關於 jQuery 源碼的全文註解,感興趣的能夠圍觀一下。jQuery v1.10.2 源碼註解 前端

系列第二篇:【深刻淺出jQuery】源碼淺析2--奇技淫巧java

 

網上已經有不少解讀 jQuery 源碼的文章了,做爲系列開篇的第一篇,思前想去起了個【深刻淺出jQuery】的標題,資歷尚淺,沒法對 jQuery 分析的頭頭是道,可是 jQuery 源碼當中確實有着大量巧妙的設計,不一樣層次水平的閱讀者都能有收穫,因此打算厚着臉皮將本身從中學到的一些知識點共享出來。打算從總體及分支,分章節剖析。本篇主要講 jQuery 的總體架構及一些前期準備,先來看看 jQuery 的總體結構:jquery

 

   jQuery 總體架構

 

不一樣於 jQuery 代碼各個模塊細節實現的晦澀難懂,jQuery 總體框架的結構十分清晰,按代碼行文大體分爲如上圖所示的模塊。git

初看 jQuery 源碼可能很容易一頭霧水,由於 9000 行的代碼感受沒有盡頭,因此瞭解做者的行文思路十分重要。github

總體而言,我以爲 jQuery 採用的是總--分的結構,雖然JavaScript有着做用域的提高機制,可是 9000 多行的代碼爲了相互的關聯性,並不表明全部的變量都要定義在最頂部。在 jQuery 中,只有全局都會用到的變量、正則表達式定義在了代碼最開頭,而每一個模塊一開始,又會定義一些只在本模塊會使用到的變量、正則、方法等。因此在一開始的閱讀的過程當中會有不少看不懂其做用的變量,正則,方法。正則表達式

因此,我以爲閱讀源碼很重要的一點是,摒棄面向過程的思惟方式,不要刻意去追求從上至下每一句都要在一開始弄明白。頗有可能一開始你在一個奇怪的方法或者變量處卡殼了,很想知道這個方法或變量的做用,然而可能它要到幾千行處才被調用到。若是去追求這種逐字逐句弄清楚的方式,頗有可能在碰壁幾回以後閱讀的積極性大受打擊。 編程

道理說了不少,接來下進入真正的正文,對 jQurey 的一些前期準備,小的細節進行分析:

 

   jQuery 閉包結構

// 用一個函數域包起來,就是所謂的沙箱
// 在這裏邊 var 定義的變量,屬於這個函數域內的局部變量,避免污染全局
// 把當前沙箱須要的外部變量經過函數參數引入進來
// 只要保證參數對內提供的接口的一致性,你還能夠隨意替換傳進來的這個參數
(function(window, undefined) {
   // jQuery 代碼
})(window);

jQuery 具體的實現,都被包含在了一個當即執行函數構造的閉包裏面,爲了避免污染全局做用域,只在後面暴露 $ 和 jQuery 這 2 個變量給外界,儘可能的避開變量衝突。經常使用的還有另外一種寫法:

(function(window) {
   // JS代碼
})(window, undefined);

比較推崇的的第一種寫法,也就是 jQuery 的寫法。兩者有何不一樣呢,當咱們的代碼運行在更早期的環境當中(pre-ES5,eg. Internet Explorer 8),undefined 僅是一個變量且它的值是能夠被覆蓋的。意味着你能夠作這樣的操做:

undefined = 42
console.log(undefined) // 42

當使用第一種方式,能夠確保你須要的 undefined 確實就是 undefined。

另外不得不提出的是,jQuery 在這裏有一個針對壓縮優化細節,使用第一種方式,在代碼壓縮的時候,window 和 undefined 均可以壓縮爲 1 個字母而且確保它們就是 window 和 undefined。

// 壓縮策略
// w -> windwow , u -> undefined
(function(w, u) {

})(window);

  

   jQuery 無 new 構造

 嘿,回想一下使用 jQuery 的時候,實例化一個 jQuery 對象的方法:

// 無 new 構造
$('#test').text('Test');

// 固然也可使用 new
var test = new $('#test');
test.text('Test');

大部分人使用 jQuery 的時候都是使用第一種無 new 的構造方式,直接 $('') 進行構造,這也是 jQuery 十分便捷的一個地方。當咱們使用第一種無 new 構造方式的時候,其本質就是至關於 new jQuery(),那麼在 jQuery 內部是如何實現的呢?看看:

(function(window, undefined) {
	var 
	// ...
	jQuery = function(selector, context) {
		// The jQuery object is actually just the init constructor 'enhanced'
		// 看這裏,實例化方法 jQuery() 其實是調用了其拓展的原型方法 jQuery.fn.init
		return new jQuery.fn.init(selector, context, rootjQuery);
	},

	// jQuery.prototype 便是 jQuery 的原型,掛載在上面的方法,便可讓全部生成的 jQuery 對象使用
	jQuery.fn = jQuery.prototype = {
		// 實例化化方法,這個方法能夠稱做 jQuery 對象構造器
		init: function(selector, context, rootjQuery) {
			// ... 
		}
	}
	// 這一句很關鍵,也很繞
	// jQuery 沒有使用 new 運算符將 jQuery 實例化,而是直接調用其函數
	// 要實現這樣,那麼 jQuery 就要當作一個類,且返回一個正確的實例
	// 且實例還要能正確訪問 jQuery 類原型上的屬性與方法
	// jQuery 的方式是經過原型傳遞解決問題,把 jQuery 的原型傳遞給jQuery.prototype.init.prototype
	// 因此經過這個方法生成的實例 this 所指向的仍然是 jQuery.fn,因此能正確訪問 jQuery 類原型上的屬性與方法
	jQuery.fn.init.prototype = jQuery.fn;

})(window);

大部分人初看 jQuery.fn.init.prototype = jQuery.fn 這一句都會被卡主,非常不解。可是這句真的算是 jQuery 的絕妙之處。理解這幾句很重要,分點解析一下:

1)首先要明確,使用 $('xxx') 這種實例化方式,其內部調用的是 return new jQuery.fn.init(selector, context, rootjQuery) 這一句話,也就是構造實例是交給了 jQuery.fn.init() 方法去完成。

2)將 jQuery.fn.init 的 prototype 屬性設置爲 jQuery.fn,那麼使用 new jQuery.fn.init() 生成的對象的原型對象就是 jQuery.fn ,因此掛載到 jQuery.fn 上面的函數就至關於掛載到 jQuery.fn.init() 生成的 jQuery 對象上,全部使用 new jQuery.fn.init() 生成的對象也可以訪問到 jQuery.fn 上的全部原型方法。

3)也就是實例化方法存在這麼一個關係鏈  

  • jQuery.fn.init.prototype = jQuery.fn = jQuery.prototype ;
  • new jQuery.fn.init() 至關於 new jQuery() ;
  • jQuery() 返回的是 new jQuery.fn.init(),而 var obj = new jQuery(),因此這 2 者是至關的,因此咱們能夠無 new 實例化 jQuery 對象。

 

   jQuery 方法的重載

jQuery 源碼晦澀難讀的另外一個緣由是,使用了大量的方法重載,可是用起來卻很方便:

// 獲取 title 屬性的值
$('#id').attr('title');
// 設置 title 屬性的值
$('#id').attr('title','jQuery');

// 獲取 css 某個屬性的值
$('#id').css('title');
// 設置 css 某個屬性的值
$('#id').css('width','200px');

方法的重載便是一個方法實現多種功能,常常又是 get 又是 set,雖然閱讀起來十分不易,可是從實用性的角度考慮,這也是爲何 jQuery 如此受歡迎的緣由,大多數人使用 jQuery() 構造方法使用的最多的就是直接實例化一個 jQuery 對象,但其實在它的內部實現中,有着 9 種不一樣的方法重載場景:

// 接受一個字符串,其中包含了用於匹配元素集合的 CSS 選擇器
jQuery([selector,[context]])
// 傳入單個 DOM 
jQuery(element)
// 傳入 DOM 數組
jQuery(elementArray)
// 傳入 JS 對象
jQuery(object)
// 傳入 jQuery 對象
jQuery(jQuery object)
// 傳入原始 HTML 的字符串來建立 DOM 元素
jQuery(html,[ownerDocument])
jQuery(html,[attributes])
// 傳入空參數
jQuery()
// 綁定一個在 DOM 文檔載入完成後執行的函數
jQuery(callback)

因此讀源碼的時候,很重要的一點是結合 jQuery API 進行閱讀,去了解方法重載了多少種功能,同時我想說的是,jQuery 源碼有些方法的實現特別長且繁瑣,由於 jQuery 自己做爲一個通用性特別強的框架,一個方法兼容了許多狀況,也容許用戶傳入各類不一樣的參數,致使內部處理的邏輯十分複雜,因此當解讀一個方法的時候感受到了明顯的困難,嘗試着跳出卡殼的那段代碼自己,站在更高的維度去思考這些複雜的邏輯是爲了處理或兼容什麼,是不是重載,爲何要這樣寫,必定會有不同的收穫。其次,也是由於這個緣由,jQuery 源碼存在許多兼容低版本的 HACK 或者邏輯十分晦澀繁瑣的代碼片斷,瀏覽器兼容這樣的大坑極其容易讓一個前端工程師不能學到編程的精髓,因此不要太執着於一些邊角料,即便兼容性很重要,也應該適度學習理解,適可而止。

 

   jQuery.fn.extend 與 jQuery.extend

extend 方法在 jQuery 中是一個很重要的方法,jQuey 內部用它來擴展靜態方法或實例方法,並且咱們開發 jQuery 插件開發的時候也會用到它。可是在內部,是存在 jQuery.fn.extend 和 jQuery.extend 兩個 extend 方法的,而區分這兩個 extend 方法是理解 jQuery 的很關鍵的一部分。先看結論:

1)jQuery.extend(object) 爲擴展 jQuery 類自己,爲類添加新的靜態方法;

2)jQuery.fn.extend(object) 給 jQuery 對象添加實例方法,也就是經過這個 extend 添加的新方法,實例化的 jQuery 對象都能使用,由於它是掛載在 jQuery.fn 上的方法(上文有提到,jQuery.fn = jQuery.prototype )。 

它們的官方解釋是:

1)jQuery.extend(): 把兩個或者更多的對象合併到第一個當中,

2)jQuery.fn.extend():把對象掛載到 jQuery 的 prototype 屬性,來擴展一個新的 jQuery 實例方法。

也就是說,使用 jQuery.extend() 拓展的靜態方法,咱們能夠直接使用 $.xxx 進行調用(xxx是拓展的方法名),

而使用 jQuery.fn.extend() 拓展的實例方法,須要使用 $().xxx 調用。

源碼解析較長,點擊下面能夠展開,也能夠去這裏閱讀

// 擴展合併函數
// 合併兩個或更多對象的屬性到第一個對象中,jQuery 後續的大部分功能都經過該函數擴展
// 雖然實現方式同樣,可是要注意區分用法的不同,那麼爲何兩個方法指向同一個函數實現,可是卻實現不一樣的功能呢,
// 閱讀源碼就能發現這歸功於 this 的強大力量
// 若是傳入兩個或多個對象,全部對象的屬性會被添加到第一個對象 target
// 若是隻傳入一個對象,則將對象的屬性添加到 jQuery 對象中,也就是添加靜態方法
// 用這種方式,咱們能夠爲 jQuery 命名空間增長新的方法,能夠用於編寫 jQuery 插件
// 若是不想改變傳入的對象,能夠傳入一個空對象:$.extend({}, object1, object2);
// 默認合併操做是不迭代的,即使 target 的某個屬性是對象或屬性,也會被徹底覆蓋而不是合併
// 若是第一個參數是 true,則是深拷貝
// 從 object 原型繼承的屬性會被拷貝,值爲 undefined 的屬性不會被拷貝
// 由於性能緣由,JavaScript 自帶類型的屬性不會合並
jQuery.extend = jQuery.fn.extend = function() {
	var src, copyIsArray, copy, name, options, clone,
		target = arguments[0] || {},
		i = 1,
		length = arguments.length,
		deep = false;

	// Handle a deep copy situation
	// target 是傳入的第一個參數
	// 若是第一個參數是布爾類型,則表示是否要深遞歸,
	if (typeof target === "boolean") {
		deep = target;
		target = arguments[1] || {};
		// skip the boolean and the target
		// 若是傳了類型爲 boolean 的第一個參數,i 則從 2 開始
		i = 2;
	}

	// Handle case when target is a string or something (possible in deep copy)
	// 若是傳入的第一個參數是 字符串或者其餘
	if (typeof target !== "object" && !jQuery.isFunction(target)) {
		target = {};
	}

	// extend jQuery itself if only one argument is passed
	// 若是參數的長度爲 1 ,表示是 jQuery 靜態方法
	if (length === i) {
		target = this;
		--i;
	}

	// 能夠傳入多個複製源
	// i 是從 1或2 開始的
	for (; i < length; i++) {
		// Only deal with non-null/undefined values
		// 將每一個源的屬性所有複製到 target 上
		if ((options = arguments[i]) != null) {
			// Extend the base object
			for (name in options) {
				// src 是源(即自己)的值
				// copy 是即將要複製過去的值
				src = target[name];
				copy = options[name];

				// Prevent never-ending loop
				// 防止有環,例如 extend(true, target, {'target':target});
				if (target === copy) {
					continue;
				}

				// Recurse if we're merging plain objects or arrays
				// 這裏是遞歸調用,最終都會到下面的 else if 分支
				// jQuery.isPlainObject 用於測試是否爲純粹的對象
				// 純粹的對象指的是 經過 "{}" 或者 "new Object" 建立的
				// 若是是深複製
				if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
					// 數組
					if (copyIsArray) {
						copyIsArray = false;
						clone = src && jQuery.isArray(src) ? src : [];

						// 對象
					} else {
						clone = src && jQuery.isPlainObject(src) ? src : {};
					}

					// 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 the modified object
	// 返回新的 target
	// 若是 i < length ,是直接返回沒通過處理的 target,也就是 arguments[0]
	// 也就是若是不傳須要覆蓋的源,調用 $.extend 實際上是增長 jQuery 的靜態方法
	return target;
};

須要注意的是這一句 jQuery.extend = jQuery.fn.extend = function() {} ,也就是 jQuery.extend 的實現和 jQuery.fn.extend 的實現共用了同一個方法,可是爲何可以實現不一樣的功能了,這就要歸功於 Javascript 強大(怪異?)的 this 了。

1)在 jQuery.extend() 中,this 的指向是 jQuery 對象(或者說是 jQuery 類),因此這裏擴展在 jQuery 上;

2)在 jQuery.fn.extend() 中,this 的指向是 fn 對象,前面有提到 jQuery.fn = jQuery.prototype ,也就是這裏增長的是原型方法,也就是對象方法。

 

   jQuery 的鏈式調用及回溯

另外一個讓你們喜好使用 jQuery 的緣由是它的鏈式調用,這一點的實現其實很簡單,只須要在要實現鏈式調用的方法的返回結果裏,返回 this ,就可以實現鏈式調用了。

固然,除了鏈式調用,jQuery 甚至還容許回溯,看看:

// 經過 end() 方法終止在當前鏈的最新過濾操做,返回上一個對象集合
$('div').eq(0).show().end().eq(1).hide();

當選擇了 ('div').eq(0) 以後使用 end() 能夠回溯到上一步選中的 jQuery 對象 $('div'),其內部實現實際上是依靠添加了 prevObject 這個屬性:

jQuery 完整的鏈式調用、增棧、回溯經過 return thisreturn this.pushStack()return this.prevObject 實現,看看源碼實現:

jQuery.fn = jQuery.prototype = { 
	// 將一個 DOM 元素集合加入到 jQuery 棧
	// 此方法在 jQuery 的 DOM 操做中被頻繁的使用, 如在 parent(), find(), filter() 中
	// pushStack() 方法經過改變一個 jQuery 對象的 prevObject 屬性來跟蹤鏈式調用中前一個方法返回的 DOM 結果集合
	// 當咱們在鏈式調用 end() 方法後, 內部就返回當前 jQuery 對象的 prevObject 屬性
	pushStack: function(elems) {
		// 構建一個新的jQuery對象,無參的 this.constructor(),只是返回引用this
		// jQuery.merge 把 elems 節點合併到新的 jQuery 對象
		// this.constructor 就是 jQuery 的構造函數 jQuery.fn.init,因此 this.constructor() 返回一個 jQuery 對象
		// 因爲 jQuery.merge 函數返回的對象是第二個函數附加到第一個上面,因此 ret 也是一個 jQuery 對象,這裏能夠解釋爲何 pushStack 出入的 DOM 對象也能夠用 CSS 方法進行操做
		var ret = jQuery.merge(this.constructor(), elems);

		// 給返回的新 jQuery 對象添加屬性 prevObject
		// 因此也就是爲何經過 prevObject 能取到上一個合集的引用了
		ret.prevObject = this;
		ret.context = this.context;

		// Return the newly-formed element set
		return ret;
	},
	// 回溯鏈式調用的上一個對象
	end: function() {
		// 回溯的關鍵是返回 prevObject 屬性
		// 而 prevObject 屬性保存了上一步操做的 jQuery 對象集合
		return this.prevObject || this.constructor(null);
	},
	// 取當前 jQuery 對象的第 i 個
	eq: function(i) {
		// jQuery 對象集合的長度
		var len = this.length,
			j = +i + (i < 0 ? len : 0);

		// 利用 pushStack 返回
		return this.pushStack(j >= 0 && j < len ? [this[j]] : []);
	},	
}

總的來講,

1)end() 方法返回 prevObject 屬性,這個屬性記錄了上一步操做的 jQuery 對象合集;

2)而 prevObject 屬性由 pushStack() 方法生成,該方法將一個 DOM 元素集合加入到 jQuery 內部管理的一個棧中,經過改變 jQuery 對象的 prevObject 屬性來跟蹤鏈式調用中前一個方法返回的 DOM 結果集合

3)當咱們在鏈式調用 end() 方法後,內部就返回當前 jQuery 對象的 prevObject 屬性,完成回溯。

 

   jQuery 正則與細節優化

不得不提 jQuery 在細節優化上作的很好。也存在不少值得學習的小技巧,下一篇將會以 jQuery 中的一些編程技巧爲主題行文,這裏就再也不贅述。

而後想談談正則表達式,jQuery 當中用了大量的正則表達式,我以爲若是研讀 jQuery ,正則水平必定可以大大提高,若是是個正則小白,我建議在閱讀以前先去了解如下幾點:

1)瞭解並嘗試使用 Javascript 正則相關 API,包括了 test() 、replace() 、match() 、exec() 的用法;

2)區分上面 4 個方法,哪一個是 RegExp 對象方法,哪一個是 String 對象方法;

3)瞭解簡單的零寬斷言,瞭解什麼是匹配可是不捕獲以及匹配而且捕獲

 

   jQuery 變量衝突處理

最後想提一提 jQuery 變量的衝突處理,經過一開始保存全局變量的 window.jQuery 以及 windw.$ 。

當須要處理衝突的時候,調用靜態方法 noConflict(),讓出變量的控制權,源碼以下:

(function(window, undefined) {
	var
		// Map over jQuery in case of overwrite
		// 設置別名,經過兩個私有變量映射了 window 環境下的 jQuery 和 $ 兩個對象,以防止變量被強行覆蓋
		_jQuery = window.jQuery,
		_$ = window.$;

	jQuery.extend({
		// noConflict() 方法讓出變量 $ 的 jQuery 控制權,這樣其餘腳本就可使用它了
		// 經過全名替代簡寫的方式來使用 jQuery 
		// deep -- 布爾值,指示是否容許完全將 jQuery 變量還原(移交 $ 引用的同時是否移交 jQuery 對象自己)
		noConflict: function(deep) {
			// 判斷全局 $ 變量是否等於 jQuery 變量
			// 若是等於,則從新還原全局變量 $ 爲 jQuery 運行以前的變量(存儲在內部變量 _$ 中)
			if (window.$ === jQuery) {
				// 此時 jQuery 別名 $ 失效
				window.$ = _$;
			}
			// 當開啓深度衝突處理而且全局變量 jQuery 等於內部 jQuery,則把全局 jQuery 還原成以前的情況
			if (deep && window.jQuery === jQuery) {
				// 若是 deep 爲 true,此時 jQuery 失效
				window.jQuery = _jQuery;
			}

			// 這裏返回的是 jQuery 庫內部的 jQuery 構造函數(new jQuery.fn.init()) 
			// 像使用 $ 同樣盡情使用它吧
			return jQuery;
		}
	})
}(window)

畫了一幅簡單的流程圖幫助理解:

jQuery衝突處理流程圖

那麼讓出了這兩個符號以後,是否就不能在咱們的代碼中使用 jQuery 或者呢 $ 呢?莫慌,仍是可使用的:

// 讓出 jQuery 、$ 的控制權不表明不能使用 jQuery 和 $ ,方法以下:
var query = jQuery.noConflict(true);

(function($) { 

// 插件或其餘形式的代碼,也能夠將參數設爲 jQuery
})(query);

//  ... 其餘用 $ 做爲別名的庫的代碼

 

   結束語

對 jQuery 總體架構的一些解析就到這裏,下一篇將會剖析一下 jQuery 中的一些優化小技巧,一些對編程有所提升的地方。

原創文章,文筆有限,才疏學淺,文中如有不正之處,萬望告知。

若是本文對你有幫助,請點下推薦,寫文章不容易。

系列第二篇:【深刻淺出jQuery】源碼淺析2--奇技淫巧

最後,我在 github 上關於 jQuery 源碼的全文註解,感興趣的能夠圍觀一下,給顆星星。jQuery v1.10.2 源碼註解 

相關文章
相關標籤/搜索