【深入淺出jQuery】原始碼淺析–整體架構

發表於2016-03-16

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

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

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

我在 github 上關於 jQuery 原始碼的全文註解,感興趣的可以圍觀一下。jQuery v1.10.2 原始碼註解 

 

網上已經有很多解讀 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。

   無 new 構造

 嘿,回想一下使用 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 或者邏輯十分晦澀繁瑣的程式碼片段,瀏覽器相容這樣的大坑極其容易讓一個前端工程師不能學到程式設計的精髓,所以不要太執著於一些邊角料,即使相容性很重要,也應該適度學習理解,適可而止。

 

   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.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 甚至還允許回溯,看看:

當選擇了 (‘div’).eq(0) 之後使用 end() 可以回溯到上一步選中的 jQuery 物件 $(‘div’),其內部實現其實是依靠新增了 prevObject 這個屬性:

jQuery 完整的鏈式呼叫、增棧、回溯通過 return thisreturn 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 原始碼註解 

 

相關文章