天天看點

【深入淺出jQuery】源碼淺析--整體架構

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

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

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

我在 github 上關于 jQuery 源碼的全文注解,感興趣的可以圍觀一下。jQuery v1.10.2 源碼注解 。

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

網上已經有很多解讀 jQuery 源碼的文章了,作為系列開篇的第一篇,思前想去起了個【深入淺出jQuery】的标題,資曆尚淺,無法對 jQuery 分析的頭頭是道,但是 jQuery 源碼當中确實有着大量巧妙的設計,不同層次水準的閱讀者都能有收獲,是以打算厚着臉皮将自己從中學到的一些知識點共享出來。打算從整體及分支,分章節剖析。本篇主要講 jQuery 的整體架構及一些前期準備,先來看看 jQuery 的整體結構:

【深入淺出jQuery】源碼淺析--整體架構

不同于 jQuery 代碼各個子產品細節實作的晦澀難懂,jQuery 整體架構的結構十厘清晰,按代碼行文大緻分為如上圖所示的子產品。

初看 jQuery 源碼可能很容易一頭霧水,因為 9000 行的代碼感覺沒有盡頭,是以了解作者的行文思路十分重要。

整體而言,我覺得 jQuery 采用的是總--分的結構,雖然JavaScript有着作用域的提升機制,但是 9000 多行的代碼為了互相的關聯性,并不代表所有的變量都要定義在最頂部。在 jQuery 中,隻有全局都會用到的變量、正規表達式定義在了代碼最開頭,而每個子產品一開始,又會定義一些隻在本子產品會使用到的變量、正則、方法等。是以在一開始的閱讀的過程中會有很多看不懂其作用的變量,正則,方法。

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

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

jQuery 具體的實作,都被包含在了一個立即執行函數構造的閉包裡面,為了不污染全局作用域,隻在後面暴露 $ 和 jQuery 這 2 個變量給外界,盡量的避開變量沖突。常用的還有另一種寫法:

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

當使用第一種方式,可以確定你需要的 undefined 确實就是 undefined。

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

  

 嘿,回想一下使用 jQuery 的時候,執行個體化一個 jQuery 對象的方法:

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

大部分人初看 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 源碼晦澀難讀的另一個原因是,使用了大量的方法重載,但是用起來卻很友善:

方法的重載即是一個方法實作多種功能,經常又是 get 又是 set,雖然閱讀起來十分不易,但是從實用性的角度考慮,這也是為什麼 jQuery 如此受歡迎的原因,大多數人使用 jQuery() 構造方法使用的最多的就是直接執行個體化一個 jQuery 對象,但其實在它的内部實作中,有着 9 種不同的方法重載場景:

是以讀源碼的時候,很重要的一點是結合 jQuery API 進行閱讀,去了解方法重載了多少種功能,同時我想說的是,jQuery 源碼有些方法的實作特别長且繁瑣,因為 jQuery 本身作為一個通用性特别強的架構,一個方法相容了許多情況,也允許使用者傳入各種不同的參數,導緻内部處理的邏輯十分複雜,是以當解讀一個方法的時候感覺到了明顯的困難,嘗試着跳出卡殼的那段代碼本身,站在更高的次元去思考這些複雜的邏輯是為了處理或相容什麼,是否是重載,為什麼要這樣寫,一定會有不一樣的收獲。其次,也是因為這個原因,jQuery 源碼存在許多相容低版本的 HACK 或者邏輯十分晦澀繁瑣的代碼片段,浏覽器相容這樣的大坑極其容易讓一個前端工程師不能學到程式設計的精髓,是以不要太執着于一些邊角料,即使相容性很重要,也應該适度學習了解,适可而止。

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.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 的原因是它的鍊式調用,這一點的實作其實很簡單,隻需要在要實作鍊式調用的方法的傳回結果裡,傳回 this ,就能夠實作鍊式調用了。

當然,除了鍊式調用,jQuery 甚至還允許回溯,看看:

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

【深入淺出jQuery】源碼淺析--整體架構

jQuery 完整的鍊式調用、增棧、回溯通過 return this 、 return this.pushStack() 、return this.prevObject 實作,看看源碼實作:

總的來說,

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

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

3)當我們在鍊式調用 end() 方法後,内部就傳回目前 jQuery 對象的 prevObject 屬性,完成回溯。

不得不提 jQuery 在細節優化上做的很好。也存在很多值得學習的小技巧,下一篇将會以 jQuery 中的一些程式設計技巧為主題行文,這裡就不再贅述。

然後想談談正規表達式,jQuery 當中用了大量的正規表達式,我覺得如果研讀 jQuery ,正則水準一定能夠大大提升,如果是個正則小白,我建議在閱讀之前先去了解以下幾點:

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

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

3)了解簡單的零寬斷言,了解什麼是比對但是不捕獲以及比對并且捕獲。

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

當需要處理沖突的時候,調用靜态方法 noConflict(),讓出變量的控制權,源碼如下:

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

【深入淺出jQuery】源碼淺析--整體架構

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

   結束語

對 jQuery 整體架構的一些解析就到這裡,下一篇将會剖析一下 jQuery 中的一些優化小技巧,一些對程式設計有所提高的地方。

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

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

最後,我在 github 上關于 jQuery 源碼的全文注解,感興趣的可以圍觀一下,給顆星星。jQuery v1.10.2 源碼注解 。