1. 程式人生 > >jquery源碼解讀

jquery源碼解讀

了解 數組 exe function 通用 jquery源碼 積極性 $() stack

最近一直在研讀 jQuery 源碼,初看源碼一頭霧水毫無頭緒,真正靜下心來細看寫的真是精妙,其結構明晰,高內聚、低耦合,兼具優秀的性能與便利的擴展性,在瀏覽器的兼容性(功能缺陷、漸進增強)優雅的處理能力以及 Ajax 等方面周到而強大的定制功能無不令人驚嘆。

jQuery 的整體結構是這樣的:

jQuery 整體架構

技術分享圖片

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

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

整體而言,我覺得 jQuery 采用的是總--分

的結構,雖然JavaScript有著作用域的提升機制,但是 9000 多行的代碼為了相互的關聯性,並不代表所有的變量都要定義在最頂部。在 jQuery 中,只有全局都會用到的變量、正則表達式定義在了代碼最開頭,而每個模塊一開始,又會定義一些只在本模塊會使用到的變量、正則、方法等。所以在一開始的閱讀的過程中會有很多看不懂其作用的變量,正則,方法。

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

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

jQuery 閉包結構

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

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

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

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

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

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

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

1 2 3 4 5 // 壓縮策略 // w -> windwow , u -> undefined (function(w, u) { })(window);

  

jQuery 無 new 構造

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

1 2 3 4 5 6 // 無 new 構造 $(‘#test‘).text(‘Test‘); // 當然也可以使用 new var test = new $(‘#test‘); test.text(‘Test‘);

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 (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 源碼晦澀難讀的另一個原因是,使用了大量的方法重載,但是用起來卻很方便:

1 2 3 4 5 6 7 8 9 // 獲取 title 屬性的值 $(‘#id‘).attr(‘title‘); // 設置 title 屬性的值 $(‘#id‘).attr(‘title‘,‘jQuery‘); // 獲取 css 某個屬性的值 $(‘#id‘).css(‘title‘); // 設置 css 某個屬性的值 $(‘#id‘).css(‘width‘,‘200px‘);

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 接受一個字符串,其中包含了用於匹配元素集合的 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 調用。

源碼解析較長,點擊下面可以展開,也可以去這裏閱讀:

+ View Code

需要註意的是這一句 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 甚至還允許回溯,看看:

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

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

技術分享圖片

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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(),讓出變量的控制權,源碼如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 (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 或者呢 $ 呢?莫慌,還是可以使用的:

1 2 3 4 5 6 7 8 9 // 讓出 jQuery 、$ 的控制權不代表不能使用 jQuery 和 $ ,方法如下: var query = jQuery.noConflict(true); (function($) { // 插件或其他形式的代碼,也可以將參數設為 jQuery })(query); // ... 其他用 $ 作為別名的庫的代碼

jquery源碼解讀