jQuery 類庫的設計
目前為止,jquery是js社區中最活躍、用戶最多的前端類庫,具有鏈式操作、兼容性、基于數組的操作、強大的插件機制等特點,也是很多前端入門同學最早接觸到的庫。但是內部如何實現的,一直吸引著我。因此最近三個月讀完了jquery1.7版本的設計,之所以選擇該版本是因為Sizzle在1.8之后引入了編譯函數,代碼變動比較大。
1.總體設計
本文對jquery1.7版本進行了閱讀學習,將整個jquery源碼拆分為11個模塊,這些模塊相互依賴,構成了一個簡單、強大的js類庫。jquery是一個基于DOM操作的類庫,因此Sizzle選擇器引擎的實現就顯得尤為重要。針對Sizzle選擇器引擎的實現,之前已經做過先關的分析,參見:sizzle選擇器引擎介紹 。下面對其中的數據存儲、事件處理、異常請求ajax、動畫等進行簡單的介紹。
(function(window, undefined) { var jQuery = function(selector, context) { return new jQuery.fn.init(selector, context, rootJquery); } jQuery.fn = function() { // 原型對象 ... } // 工具方法 // 異步隊列 // 隊列queue // 瀏覽器測試support // 屬性操作 // 事件系統 // DOM遍歷與操作、樣式操作 // ajax請求 // 動畫 })(window)
2. 數據存儲
在實際的項目開發中,經常需要把某些信息附加到一個DOM節點中,那么如果管理DOM節點和附加數據的關系,就顯得非常重要。很明顯,目前有兩種思路來解決這個問題:1)直接附加在DOM節點上;2)通過一個id來關聯DOM節點、附加數據。這兩種方法各有利弊:方法1的好處是DOM節點、附加數據在一起,便于維護;方法2的好處是可以避免相互依賴,從而避免內存泄露問題。
var arr = []; function createNode() { return document.createElement('div'); } function saveNodes() { for(var i=0; i<100; i++) { arr.push(createNode()); } } // 將上述100個nodes節點渲染到頁面,之后在將其從頁面中剔除掉,那么因為arr引用著100個nodes節點,這些節點就無法被垃圾回收機制回收,從而引起內存泄露問題。
3. 異步Deferred
jQuery中關于異步的實現,大致上遵循Promise/A規范,為什么說是大致呢?
- then的返回:then方法并沒有返回新的異步對象,這不符合Promise/A規范中的then要求。這一點在jQuery2.0以后已經被修改
- 結果處理:在進行結果處理的時候,jQuery并沒有進行結果的異常捕獲
- 參數的個數:jquery中resolve可以有多個參數,而Promise/A的resolve僅能有一個 </ol>
- 參數的設置:在jQuery所有API中,.ajax的參數種類應該是對多的,里面的參數看得人掩護繚亂。但是其中最終的有url、type、dataType、data,尤其是dataType的設置對于結果的應該很長大,所以有大量代碼是對這一步的處理。
- 前置過濾函數處理:主要是對json、jsonp、script三種數據類型的處理,在請求發送前對其進行過濾處理
- 請求發出:這里請求的發出包括兩種方式,分別為依賴于XMLHttpRequest和script標簽。如果瀏覽器允許跨域,則僅需使用XMLHttpRequest就足夠了。
- 回調函數的執行:這里的回調函數包括很多種,請求發出前、開始接受數據、數據接受完成等等。 </ol>
- 參數配置:主要有三個參數,duration表示動畫的執行時間;easing為動畫每一幀的變化速度,目前jQuery中僅存在兩種幀變化函數:線性(linear)變化、余弦變化(swing),要用其他變化函數只能修改$.fx.easing對象;complete為動畫完成之后需要執行的回調函數。
- 生成動畫函數doAnimation:jQuery會給每一個樣式生成一個$.fx對象,該對象用于實現動畫效果。默認情況下,每隔13ms會執行一幀動畫,然后更新頁面的樣式。
- 動畫執行:如果動畫不需要排隊,在生成完動畫函數之后,就立即執行動畫函數doAnimation;反之,將doAnimation放入隊列queue中進行排隊。當所有動畫均完成之后,就可以執行回調函數complete了。 </ol>
內部實現比較曲折抽象,代碼晦澀難懂,主要是通過”once”、”memory”兩個參數進行控制:”once”決定異步的回調函數只能被執行一次;“memory”決定函數具有記憶動能,也就是異步事件完成以后,再綁定回調函數,回調函數會立即執行。
4. 事件處理
jquery內部事件的功能很強大,除了可以處理DOM事件外,還可以自定義事件、觸發事件(DOM事件、自定義事件)、定義事件的命名空間等強大功能。并且jquery內部的另外一大亮點就是通過數據存儲模塊(.data),盡量降低DOM和監聽事件之間的依賴,避免DOM、js對象相互依賴造成的內存泄露。在數據緩存模塊的數據結構如下:
$('.a').on('click', function() {}); // results is as fellow: $.cache = { 1: { events: { click: [ //click.delegateCount: 記錄代理事件的個數,代理回調函數放在數組的前面 { data: ..., guid: ..., selector: ..., handler: function() {}, // handler.guid = 1 用于定位和移除監聽函數 .... } ] }, handle: function() {......} // 主監聽函數 } }
4.1 事件綁定:
當綁定事件時,內部方法的調用鏈為:bind/delegate/live/one()—>.on()—>$.event.add()—>$.data()/addEventListener/attachEvent()。其實在對于一個DOM元素,所有的事件都對應一個主監聽函數($._data(elem).handle),然后通過主監聽函數通過事件分發函數($.event.dispatch)來觸發相應類型的監聽函數。
$('.a').on('click', fn1); $('.a').on('blur', fn2); $('.a').on('focus', fn3); // 其實DOM元素(.a)并沒有直接與fn1、fn2、fn3關聯起來,而是通過dispatch進行事件分發 // 用偽代碼可以表示如下: $('.a').on('click blur focus', dispatch); function dispatch(type) { var fn; if(type === 'click') { fn = fn1; } else if(type === 'blur') { fn = fn2; } else if (type === 'focus') { fn = fn3; } return fn; }
4. 2 事件移除
當移除事件時,內部方法的調用鏈為:unbind/undelagate/die()—>.off()—>$.event.remove()—>$._data()/removeEventListener/detachEvent()。事件的移除,也就是從數據存儲對象$.cache中移除相應的事件對象,當事件對象events為空時,則移除整個數據緩存對象。
4.3 事件手動觸發
在jquery中,可以手工觸發DOM事件或自定義事件。內部方法的調用鏈為:.trigger/triggerHandler() —> $.event.trigger() — > $.event.dispatch(主監聽函數) — >事件的監聽函數。
還有一個需要關注的問題就是,如何事件事件的冒泡呢?方法其實很簡單,就是根據DOM的結構向上查詢出元素的祖先元素,一直到window對象,這樣就構成了元素的冒泡路徑,然后觸發這個路徑上元素的相應事件。這也是jquery可以模擬focus、blur、change、submit進行事件冒泡的關鍵環節。
// 尋找冒泡路徑 var eventPath = []; for(;cur; cur = cur.parentNode) { eventPath.push([cur, type]); } // 執行路徑上的監聽函數 for() { cur = eventPath[i][0]; type = eventPath[i][1]; hanle = $._data(cur, "events")[type]; hanle.apply(cur); }
5. 異步請求ajax
異步請求是jquery在總體可以分為三部分:核心實現、便捷方法、ajax全局事件。其中該模塊依賴于Deferred模塊提供的異步編程模塊,可以方便進行回調函數的注冊,例如:
$(url, options).then(successFn, failFn);
其中核心方法的實現主要包括以下驟:
6. 動畫解析
在jQuery中,動畫show、hide、fadeIn、fadeOut等均要調用Animation方法,也就是說Animation是最基本的入口函數。在上圖中可以看到該入口函數包括了三個過程:參數配置、生成動畫函數、動畫函數執行。下面展開包括以下細節:
當然,jQuery內部實現比較復雜,考慮了函數暫停、樣式臨時修改(修改inline元素的width/height時,會臨時將其display修改為inline-block)、清空動畫隊列等操作。