jQuery 中的編程範式

瀏覽器前端編程的面貌自2005年以來已經發生了深入的變化,這並不簡單的意味着出現了大量功能豐富的基礎庫,使得咱們能夠更加方便的編寫業務代碼,更重要的是咱們看待前端技術的觀念發生了重大轉變,明確意識到了如何之前端特有的方式釋放程序員的生產力。本文將結合jQuery源碼的實現原理,對javascript中涌現出的編程範式和經常使用技巧做一簡單介紹。javascript

1. AJAX: 狀態駐留,異步更新
首先來看一點歷史。
A. 1995年Netscape公司的Brendan Eich開發了javacript語言,這是一種動態(dynamic)、弱類型(weakly typed)、基於原型(prototype-based)的腳本語言。
B. 1999年微軟IE5發佈,其中包含了XMLHTTP ActiveX控件。
C. 2001年微軟IE6發佈,部分支持DOM level 1和CSS 2標準。
D. 2002年Douglas Crockford發明JSON格式。
至此,能夠說Web2.0所依賴的技術元素已經基本成形,可是並無馬上在整個業界產生重大的影響。儘管一些「頁面異步局部刷新」的技巧在程序員中間祕密的流傳,甚至催生了bindows這樣龐大臃腫的類庫,但總的來講,前端被看做是貧瘠而又骯髒的沼澤地,只有後臺技術纔是王道。到底還缺乏些什麼呢?
當咱們站在今天的角度去回顧2005年以前的js代碼,包括那些當時的牛人所寫的代碼,能夠明顯的感覺到它們在程序控制力上的孱弱。並非說2005年以前的js技術自己存在問題,只是它們在概念層面上是人心渙散,缺少統一的觀念,或者說缺乏本身獨特的風格, 本身的靈魂。當時大多數的人,大多數的技術都試圖在模擬傳統的面嚮對象語言,利用傳統的面向對象技術,去實現傳統的GUI模型的仿製品。
2005年是變革的一年,也是創造概念的一年。伴隨着Google一系列讓人耳目一新的交互式應用的發佈,Jesse James Garrett的一篇文章《Ajax: A New Approach to Web Applications》被廣爲傳播。Ajax這一前端特有的概念迅速將衆多分散的實踐統一在同一口號之下,引起了Web編程範式的轉換。所謂名不正則言不順,這下無名羣衆可找到組織了。在未有Ajax以前,人們早已認識到了B/S架構的本質特徵在於瀏覽器和服務器的狀態空間是分離的,可是通常的解決方案都是隱藏這一區分,將前臺狀態同步到後臺,由後臺統一進行邏輯處理,例如ASP.NET。由於缺少成熟的設計模式支持前臺狀態駐留,在換頁的時候,已經裝載的js對象將被迫被丟棄,這樣誰還能期望它去完成什麼複雜的工做嗎?
Ajax明確提出界面是局部刷新的,前臺駐留了狀態,這就促成了一種須要:須要js對象在前臺存在更長的時間。這也就意味着須要將這些對象和功能有效的管理起來,意味着更復雜的代碼組織技術,意味着對模塊化,對公共代碼基的渴求。
jQuery現有的代碼中真正與Ajax相關(使用XMLHTTP控件異步訪問後臺返回數據)的部分其實不多,可是若是沒有Ajax, jQuery做爲公共代碼基也就缺少存在的理由。html

2. 模塊化:管理名字空間
當大量的代碼產生出來之後,咱們所須要的最基礎的概念就是模塊化,也就是對工做進行分解和複用。工做得以分解的關鍵在於各人獨立工做的成果能夠集成在一塊兒。這意味着各個模塊必須基於一致的底層概念,能夠實現交互,也就是說應該基於一套公共代碼基,屏蔽底層瀏覽器的不一致性,並實現統一的抽象層,例如統一的事件管理機制等。比統一代碼基更重要的是,各個模塊之間必須沒有名字衝突。不然,即便兩個模塊之間沒有任何交互,也沒法共同工做。
jQuery目前鼓吹的主要賣點之一就是對名字空間的良好控制。這甚至比提供更多更完善的功能點都重要的多。良好的模塊化容許咱們複用任何來源的代碼,全部人的工做得以積累疊加。而功能實現僅僅是一時的工做量的問題。jQuery使用module pattern的一個變種來減小對全局名字空間的影響,僅僅在window對象上增長了一個jQuery對象(也就是$函數)。
所謂的module pattern代碼以下,它的關鍵是利用匿名函數限制臨時變量的做用域。前端

01 var feature =(function() {
02  
03 // 私有變量和函數
04 var privateThing = ’secret’,
05     publicThing = ’not secret’,
06  
07     changePrivateThing = function() {
08         privateThing = ’super secret’;
09     },
10  
11     sayPrivateThing = function() {
12         console.log(privateThing);
13         changePrivateThing();
14     };
15  
16 // 返回對外公開的API
17 return {
18     publicThing : publicThing,
19     sayPrivateThing :  sayPrivateThing
20 }
21 })();

 

js自己缺少包結構,不過通過多年的嘗試以後業內已經逐漸統一了對包加載的認識,造成了RequireJs庫這樣獲得必定共識的解決方案。jQuery能夠與RequireJS庫良好的集成在一塊兒, 實現更完善的模塊依賴管理。http://requirejs.org/docs/jquery.htmljava

1 require(["jquery", "jquery.my"], function() {
2     //當jquery.js和jquery.my.js都成功裝載以後執行
3     $(function(){
4       $(‘#my’).myFunc();
5     });
6   });

 

經過如下函數調用來定義模塊my/shirt, 它依賴於my/cart和my/inventory模塊,node

01 require.def(「my/shirt」,
02   ["my/cart", "my/inventory"],
03   function(cart, inventory) {
04       // 這裏使用module pattern來返回my/shirt模塊對外暴露的API
05       return {
06           color: 」blue」,
07           size: 」large」
08           addToCart: function() {
09               // decrement是my/inventory對外暴露的API
10               inventory.decrement(this);
11               cart.add(this);
12           }
13       }
14   }
15 );

 

3. 神奇的$:對象提高
當你第一眼看到$函數的時候,你想到了什麼?傳統的編程理論老是告訴咱們函數命名應該準確,應該清晰無誤的表達做者的意圖,甚至聲稱長名字要優於短名字,由於減小了出現歧義的可能性。可是,$是什麼?亂碼?它所傳遞的信息實在是太隱晦,太曖昧了。$是由prototype.js庫發明的,它真的是一個神奇的函數,由於它能夠將一個原始的DOM節點提高(enhance)爲一個具備複雜行爲的對象。在prototype.js最初的實現中,$函數的定義爲jquery

1 var $ = function (id) {
2   return 」string」 == typeof id ? document.getElementById(id) : id;
3 };

 

這基本對應於以下公式程序員

1 e = $(id)

 

這毫不僅僅是提供了一個聰明的函數名稱縮寫,更重要的是在概念層面上創建了文本id與DOM element之間的一一對應。在未有$以前,id與對應的element之間的距離十分遙遠,通常要將element緩存到變量中,例如web

1 var ea = docuement.getElementById(‘a’);
2 var eb = docuement.getElementById(‘b’);
3 ea.style….

 

可是使用$以後,卻隨處可見以下的寫法ajax

1 $(‘header_’+id).style…
2 $(‘body_’+id)….

 

id與element之間的距離彷佛被消除了,能夠很是緊密的交織在一塊兒。
prototype.js後來擴展了$的含義,編程

01 function $() {
02   var elements = new Array();
03  
04   for (var i = 0; i < arguments.length; i++) {
05       var element = arguments[i];
06       if (typeof element == ’string’)
07         element = document.getElementById(element);
08  
09       if (arguments.length == 1)
10         return element;
11  
12       elements.push(element);
13   }
14  
15   return elements;
16 }

 

這對應於公式

1 [e,e] = $(id,id)

 

很遺憾,這一步prototype.js走偏了,這一作法不多有實用的價值。
真正將$發揚光大的是jQuery, 它的$對應於公式

1 [o] = $(selector)

 

這裏有三個加強
A. selector再也不是單一的節點定位符,而是複雜的集合選擇符
B. 返回的元素不是原始的DOM節點,而是通過jQuery進一步加強的具備豐富行爲的對象,能夠啓動複雜的函數調用鏈。
C. $返回的包裝對象被造型爲數組形式,將集合操做天然的整合到調用鏈中。

固然,以上僅僅是對神奇的$的一個過度簡化的描述,它的實際功能要複雜得多. 特別是有一個很是經常使用的直接構造功能

1 $(「<table><tbody><tr><td>…</td></tr></tbody></table>」)….

 

jQuery將根據傳入的html文本直接構造出一系列的DOM節點,並將其包裝爲jQuery對象. 這在某種程度上能夠看做是對selector的擴展: html內容描述自己就是一種惟一指定.
$(function{})這一功能就實在是讓人有些無語了, 它表示當document.ready的時候調用此回調函數。真的,$是一個神奇的函數, 有任何問題,請$一下。
總結起來, $是從普通的DOM和文本描述世界到具備豐富對象行爲的jQuery世界的躍遷通道。跨過了這道門,就來到了理想國。

4. 無定形的參數:專一表達而不是約束
弱類型語言既然頭上頂着個」弱」字, 總不免讓人有些先天不足的感受. 在程序中缺少類型約束, 是否真的是一種重大的缺憾? 在傳統的強類型語言中, 函數參數的類型,個數等都是由編譯器負責檢查的約束條件, 但這些約束仍然是遠遠不夠的. 通常應用程序中爲了增強約束, 總會增長大量防護性代碼, 例如在C++中咱們經常使用ASSERT, 而在java中也常常須要判斷參數值的範圍

1 if (index < 0 || index >= size)
2     throw new IndexOutOfBoundsException(
3         」Index: 」+index+」, Size: 」+size);

 

很顯然, 這些代碼將致使程序中存在大量無功能的執行路徑, 即咱們作了大量判斷, 代碼執行到某個點, 系統拋出異常, 大喊此路不通. 若是咱們換一個思路, 既然已經作了某種判斷,可否利用這些判斷的結果來作些什麼呢? javascript是一種弱類型的語言,它是沒法自動約束參數類型的, 那若是順勢而行,進一步弱化參數的形態, 將」弱」推動到一種極致, 在弱無可弱的時候, weak會不會成爲標誌性的特色?
看一下jQuery中的事件綁定函數bind,
A. 一次綁定一個事件 $(「#my」).bind(「mouseover」, function(){});
B. 一次綁定多個事件 $(「#my」).bind(「mouseover mouseout」,function(){})
C. 換一個形式, 一樣綁定多個事件
$(「#my」).bind({mouseover:function(){}, mouseout:function(){});
D. 想給事件監聽器傳點參數
$(‘#my’).bind(‘click’, {foo: 「xxxx」}, function(event) { event.data.foo..})
E. 想給事件監聽器分個組
$(「#my」).bind(「click.myGroup″, function(){});
F. 這個函數爲何尚未瘋掉???

就算是類型不肯定, 在固定位置上的參數的意義總要是肯定的吧? 退一萬步來講, 就算是參數位置不重要了,函數自己的意義應該是肯定的吧? 但這是什麼

1 //取值
2 value = o.val(),
3 //設置值
4 o.val(3)

 

一個函數怎麼能夠這樣過度, 怎麼能根據傳入參數的類型和個數不一樣而行爲不一樣呢? 看不順眼是否是? 可這就是俺們的價值觀. 既然不能防止, 那就故意容許. 雖然形式多變, 卻無一句廢話. 缺乏約束, 不妨礙表達(我不是出來嚇人的).

5. 鏈式操做: 線性化的逐步細化
jQuery早期最主要的賣點就是所謂的鏈式操做(chain).

1 $(‘#content’) // 找到content元素
2     .find(‘h3′) // 選擇全部後代h3節點
3     .eq(2)      // 過濾集合, 保留第三個元素
4         .html(‘改變第三個h3的文本’)
5     .end()      // 返回上一級的h3集合
6     .eq(0)
7         .html(‘改變第一個h3的文本’);

 

在通常的命令式語言中, 咱們總須要在重重嵌套循環中過濾數據, 實際操做數據的代碼與定位數據的代碼糾纏在一塊兒. 而jQuery採用先構造集合而後再應用函數於集合的方式實現兩種邏輯的解耦, 實現嵌套結構的線性化. 實際上, 咱們並不須要藉助過程化的思想就能夠很直觀的理解一個集合, 例如 $(‘div.my input:checked’)能夠看做是一種直接的描述,而不是對過程行爲的跟蹤.
循環意味着咱們的思惟處於一種反覆迴繞的狀態, 而線性化以後則沿着一個方向直線前進, 極大減輕了思惟負擔, 提升了代碼的可組合性. 爲了減小調用鏈的中斷, jQuery發明了一個絕妙的主意: jQuery包裝對象自己相似數組(集合). 集合能夠映射到新的集合, 集合能夠限制到本身的子集合,調用的發起者是集合,返回結果也是集合,集合能夠發生結構上的某種變化但它仍是集合, 集合是某種概念上的不動點,這是從函數式語言中吸收的設計思想。集合操做是太常見的操做, 在java中咱們很容易發現大量所謂的封裝函數其實就是在封裝一些集合遍歷操做, 而在jQuery中集合操做由於太直白而不須要封裝.
鏈式調用意味着咱們始終擁有一個「當前」對象,全部的操做都是針對這一當前對象進行。這對應於以下公式

1 x += dx

 

調用鏈的每一步都是對當前對象的增量描述,是針對最終目標的逐步細化過程。Witrix平臺中對這一思想也有着普遍的應用。特別是爲了實現平臺機制與業務代碼的融合,平臺會提供對象(容器)的缺省內容,而業務代碼能夠在此基礎上進行逐步細化的修正,包括取消缺省的設置等。
話說回來, 雖然表面上jQuery的鏈式調用很簡單, 內部實現的時候卻必須本身多寫一層循環, 由於編譯器並不知道」自動應用於集合中每一個元素」這回事.

1 $.fn['someFunc'] = function(){
2     return this.each(function(){
3       jQuery.someFunc(this,…);
4     }
5   }

 

6. data: 統一數據管理
做爲一個js庫,它必須解決的一個大問題就是js對象與DOM節點之間的狀態關聯與協同管理問題。有些js庫選擇以js對象爲主,在js對象的成員變量中保存DOM節點指針,訪問時老是以js對象爲入口點,經過js函數間接操做DOM對象。在這種封裝下,DOM節點其實只是做爲界面展示的一種底層「彙編」 而已。jQuery的選擇與Witrix平臺相似,都是以HTML自身結構爲基礎,經過js加強(enhance)DOM節點的功能,將它提高爲一個具備複雜行爲的擴展對象。這裏的思想是非侵入式設計(non-intrusive)和優雅退化機制(graceful degradation)。語義結構在基礎的HTML層面是完整的,js的做用是加強了交互行爲,控制了展示形式。
若是每次咱們都經過$(‘#my’)的方式來訪問相應的包裝對象,那麼一些須要長期保持的狀態變量保存在什麼地方呢?jQuery提供了一個統一的全局數據管理機制。

1 //獲取數據
2 $(‘#my’).data(‘myAttr’)  
3 //設置數據
4 $(‘#my’).data(‘myAttr’,3);

 

這一機制天然融合了對HTML5的data屬性的處理

1 <input id=」my」 data-my-attr=」4″ … />

 

經過 $(‘#my’).data(‘myAttr’)將能夠讀取到HTML中設置的數據。

第一次訪問data時,jQuery將爲DOM節點分配一個惟一的uuid, 而後設置在DOM節點的一個特定的expando屬性上, jQuery保證這個uuid在本頁面中不重複。

1 elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];

 

以上代碼能夠同時處理DOM節點和純js對象的狀況。若是是js對象,則data直接放置在js對象自身中,而若是是DOM節點,則經過cache統一管理。
由於全部的數據都是經過data機制統一管理的,特別是包括全部事件監聽函數(data.events),所以jQuery能夠安全的實現資源管理。在 clone節點的時候,能夠自動clone其相關的事件監聽函數。而當DOM節點的內容被替換或者DOM節點被銷燬的時候,jQuery也能夠自動解除事件監聽函數, 並安全的釋放相關的js數據。

7. event:統一事件模型
「事件沿着對象樹傳播」這一圖景是面向對象界面編程模型的精髓所在。對象的複合構成對界面結構的一個穩定的描述,事件不斷在對象樹的某個節點發生,並經過冒泡機制向上傳播。對象樹很天然的成爲一個控制結構,咱們能夠在父節點上監聽全部子節點上的事件,而不用明確與每個子節點創建關聯。
jQuery除了爲不一樣瀏覽器的事件模型創建了統一抽象以外,主要作了以下加強:
A. 增長了自定製事件(custom)機制. 事件的傳播機制與事件內容自己原則上是無關的, 所以自定製事件徹底能夠和瀏覽器內置事件經過同一條處理路徑, 採用一樣的監聽方式. 使用自定製事件能夠加強代碼的內聚性, 減小代碼耦合. 例如若是沒有自定製事件, 關聯代碼每每須要直接操做相關的對象

1 $(‘.switch, .clapper’).click(function() {
2     var $light = $(this).parent().find(‘.lightbulb’);
3     if ($light.hasClass(‘on’)) {
4         $light.removeClass(‘on’).addClass(‘off’);
5     } else {
6         $light.removeClass(‘off’).addClass(‘on’);
7     }
8   });

 

而若是使用自定製事件,則表達的語義更加內斂明確,

1 $(‘.switch, .clapper’).click(function() {
2   $(this).parent().find(‘.lightbulb’).trigger(‘changeState’);
3 });

 

B. 增長了對動態建立節點的事件監聽. bind函數只能將監聽函數註冊到已經存在的DOM節點上. 例如

1 $(‘li.trigger’).bind(‘click’,function(){}}

 

若是調用bind以後,新建了另外一個li節點,則該節點的click事件不會被監聽.
jQuery的delegate機制能夠將監聽函數註冊到父節點上, 子節點上觸發的事件會根據selector被自動派發到相應的handlerFn上. 這樣一來如今註冊就能夠監聽將來建立的節點.

1 $(‘#myList’).delegate(‘li.trigger’, ’click’, handlerFn);

 

最近jQuery1.7中統一了bind, live和delegate機制, 天下一統, 只有on/off.

1 $(‘li.trigger’).on(‘click’, handlerFn);  // 至關於bind
2 $(‘#myList’).on(‘click’, ’li.trigger’, handlerFn);  // 至關於delegate

 

 
8. 動畫隊列:全局時鐘協調
拋開jQuery的實現不談, 先考慮一下若是咱們要實現界面上的動畫效果, 到底須要作些什麼? 好比咱們但願將一個div的寬度在1秒鐘以內從100px增長到200px. 很容易想見, 在一段時間內咱們須要不時的去調整一下div的寬度, [同時]咱們還須要執行其餘代碼. 與通常的函數調用不一樣的是, 發出動畫指令以後, 咱們不能期待馬上獲得想要的結果, 並且咱們不能原地等待結果的到來. 動畫的複雜性就在於:一次性表達以後要在一段時間內執行,並且有多條邏輯上的執行路徑要同時展開, 如何協調?
偉大的艾薩克.牛頓爵士在《天然哲學的數學原理》中寫道:」絕對的、真正的和數學的時間自身在流逝着」. 全部的事件能夠在時間軸上對齊, 這就是它們內在的協調性. 所以爲了從步驟A1執行到A5, 同時將步驟B1執行到B5, 咱們只須要在t1時刻執行[A1, B1], 在t2時刻執行[A2,B2], 依此類推.

1 t1 | t2 | t3 | t4 | t5 …
2 A1 | A2 | A3 | A4 | A5 …
3 B1 | B2 | B3 | B4 | B5 …

 

具體的一種實現形式能夠是
A. 對每一個動畫, 將其分裝爲一個Animation對象, 內部分紅多個步驟.

1 animation = new Animation(div,」width」,100,200,1000,
2 //負責步驟切分的插值函數,動畫執行完畢時的回調函數);

 

B. 在全局管理器中註冊動畫對象

1 timerFuncs.add(animation);

 

C. 在全局時鐘的每個觸發時刻, 將每一個註冊的執行序列推動一步, 若是已經結束, 則從全局管理器中刪除.

1 for each animation in timerFuncs
2    if(!animation.doOneStep())
3       timerFuncs.remove(animation)

 

解決了原理問題,再來看看錶達問題, 怎樣設計接口函數纔可以以最緊湊形式表達咱們的意圖? 咱們常常須要面臨的實際問題:
A. 有多個元素要執行相似的動畫
B. 每一個元素有多個屬性要同時變化
C. 執行完一個動畫以後開始另外一個動畫
jQuery對這些問題的解答能夠說是榨盡了js語法表達力的最後一點剩餘價值.

01 $(‘input’)
02   .animate({left:’+=200px’,top:’300′},2000)
03   .animate({left:’-=200px’,top:20},1000)
04   .queue(function(){
05     // 這裏dequeue將首先執行隊列中的後一個函數,所以alert(「y」)
06     $(this).dequeue();
07     alert(‘x’);
08    })
09   .queue(function(){
10      alert(「y」);
11      // 若是不主動dequeue, 隊列執行就中斷了,不會自動繼續下去.
12      $(this).dequeue();
13    });

 

A. 利用jQuery內置的selector機制天然表達對一個集合的處理.
B. 使用Map表達多個屬性變化
C. 利用微格式表達領域特定的差量概念. ‘+=200px’表示在現有值的基礎上增長200px
D. 利用函數調用的順序自動定義animation執行的順序: 在後面追加到執行隊列中的動畫天然要等前面的動畫徹底執行完畢以後再啓動.

jQuery動畫隊列的實現細節大概以下所示,
A. animate函數實際是調用queue(function(){執行結束時須要調用dequeue,不然不會驅動下一個方法})
queue函數執行時, 若是是fx隊列, 而且當前沒有正在運行動畫(若是連續調用兩次animate,第二次的執行函數將在隊列中等待),則會自動觸發dequeue操做, 驅動隊列運行.
若是是fx隊列, dequeue的時候會自動在隊列頂端加入」inprogress」字符串,表示將要執行的是動畫.
B. 針對每個屬性,建立一個jQuery.fx對象。而後調用fx.custom函數(至關於start)來啓動動畫。
C. custom函數中將fx.step函數註冊到全局的timerFuncs中,而後試圖啓動一個全局的timer.
timerId = setInterval( fx.tick, fx.interval );
D. 靜態的tick函數中將依次調用各個fx的step函數。step函數中經過easing計算屬性的當前值,而後調用fx的update來更新屬性。
E. fx的step函數中判斷若是全部屬性變化都已完成,則調用dequeue來驅動下一個方法。

頗有意思的是, jQuery的實現代碼中明顯有不少是接力觸發代碼: 若是須要執行下一個動畫就取出執行, 若是須要啓動timer就啓動timer等. 這是由於js程序是單線程的,真正的執行路徑只有一條,爲了保證執行線索不中斷, 函數們不得不互相幫助一下. 能夠想見, 若是程序內部具備多個執行引擎, 甚至無限多的執行引擎, 那麼程序的面貌就會發生本質性的改變. 而在這種情形下, 遞歸相對於循環而言會成爲更天然的描述.

9. promise模式:因果關係的識別
現實中,總有那麼多時間線在獨立的演化着, 人與物在時空中交錯,卻沒有發生因果. 軟件中, 函數們在源代碼中排着隊, 不免會產生一些疑問, 憑什麼排在前面的要先執行? 難道沒有它就沒有我? 讓全宇宙喊着1,2,3齊步前進, 從上帝的角度看,大概是管理難度過大了, 因而便有了相對論. 若是相互之間沒有交換信息, 沒有產生相互依賴, 那麼在某個座標系中順序發生的事件, 在另一個座標系中看來, 就多是顛倒順序的. 程序員依葫蘆畫瓢, 便發明了promise模式.
promise與future模式基本上是一回事,咱們先來看一下java中熟悉的future模式.

1 futureResult = doSomething();
2
相關文章
相關標籤/搜索