1. 程式人生 > >jquery中的程式設計正規化,即jquery的牛逼之處

jquery中的程式設計正規化,即jquery的牛逼之處

轉自:http://www.iteye.com/topic/1119283

對jquery理解比較深,積累一下,整理了一下格式,就當練習一下 markdown 語法.

本文將結合jQuery原始碼的實現原理,對javascript中湧現出的程式設計正規化和常用技巧作一簡單介紹。

1. AJAX: 狀態駐留,非同步更新

首先來看一點歷史。

  • 1995年Netscape公司的Brendan Eich開發了javacript語言,這是一種動態(dynamic)、弱型別(weakly typed)、基於原型(prototype-based)的指令碼語言。

  • 1999年微軟IE5釋出,其中包含了XMLHTTP ActiveX控制元件。
  • 2001年微軟IE6釋出,部分支援DOM level 1和CSS 2標準。
  • 2002年Douglas Crockford發明JSON格式。

    至此,可以說Web2.0所依賴的技術元素已經基本成形,但是並沒有立刻在整個業界產生重大的影響。儘管一些“頁面非同步區域性重新整理”的技巧在程式設計師中間祕密的流傳,甚至催生了bindows這樣龐大臃腫的類庫,但總的來說,前端被看作是貧瘠而又骯髒的沼澤地,只有後臺技術才是王道。到底還缺少些什麼呢?
    當我們站在今天的角度去回顧2005年之前的js程式碼,包括那些當時的牛人所寫的程式碼,可以明顯的感受到它們在程式控制力上的孱弱。並不是說2005年之前的js技術本身存在問題,只是它們在概念層面上是一盤散沙,缺乏統一的觀念,或者說缺少自己獨特的風格, 自己的靈魂。當時大多數的人,大多數的技術都試圖在模擬傳統的面嚮物件語言,利用傳統的面向物件技術,去實現傳統的GUI模型的仿製品。

  • 2005,ajax問世。

    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作為公共程式碼基也就缺乏存在的理由。

2. 模組化:管理名字空間

當大量的程式碼產生出來以後,我們所需要的最基礎的概念就是模組化,也就是對工作進行分解和複用。工作得以分解的關鍵在於各人獨立工作的成果可以整合在一起。這意味著各個模組必須基於一致的底層概念,可以實現互動,也就是說應該基於一套公共程式碼基,遮蔽底層瀏覽器的不一致性,並實現統一的抽象層,例如統一的事件管理機制等。比統一程式碼基更重要的是,各個模組之間必須沒有名字衝突。否則,即使兩個模組之間沒有任何互動,也無法共同工作。

jQuery目前鼓吹的主要賣點之一就是對名字空間的良好控制。這甚至比提供更多更完善的功能點都重要的多。良好的模組化允許我們複用任何來源的程式碼,所有人的工作得以積累疊加。而功能實現僅僅是一時的工作量的問題。jQuery使用module pattern的一個變種來減少對全域性名字空間的影響,僅僅在window物件上增加了一個jQuery物件(也就是$函式)。
所謂的module pattern程式碼如下,它的關鍵是利用匿名函式限制臨時變數的作用域。

var feature =(function() {  
  
    // 私有變數和函式  
    var privateThing = 'secret',  
        publicThing = 'not secret',  
  
    changePrivateThing = function() {  
        privateThing = 'super secret';  
    },  
  
    sayPrivateThing = function() {  
        console.log(privateThing);  
        changePrivateThing();  
    };  
  
    // 返回對外公開的API  
    return {  
        publicThing : publicThing,  
        sayPrivateThing :  sayPrivateThing  
    }  
})();  

js本身缺乏包結構,不過經過多年的嘗試之後業內已經逐漸統一了對包載入的認識,形成了RequireJs庫這樣得到一定共識的解決方案。jQuery可以與RequireJS庫良好的整合在一起, 實現更完善的模組依賴管理。http://requirejs.org/docs/jquery.html

require(["jquery", "jquery.my"], function() {  
    //當jquery.js和jquery.my.js都成功裝載之後執行  
    $(function(){  
      $('#my').myFunc();  
    });  
});  

通過以下函式呼叫來定義模組my/shirt, 它依賴於my/cart和my/inventory模組,

require.def("my/shirt",["my/cart", "my/inventory"], 
    function(cart, inventory) {  
        // 這裡使用module pattern來返回my/shirt模組對外暴露的API  
        return {  
            color: "blue",  
            size: "large"  
            addToCart: function() {  
                // decrement是my/inventory對外暴露的API  
                inventory.decrement(this);  
                cart.add(this);  
             }  
        }  
    }  
);  

3. 神奇的$:物件提升

當你第一眼看到$函式的時候,你想到了什麼?傳統的程式設計理論總是告訴我們函式命名應該準確,應該清晰無誤的表達作者的意圖,甚至聲稱長名字要優於短名字,因為減少了出現歧義的可能性。但是,$是什麼?亂碼?它所傳遞的資訊實在是太隱晦,太曖昧了。$是由prototype.js庫發明的,它真的是一個神奇的函式,因為它可以將一個原始的DOM節點提升(enhance)為一個具有複雜行為的物件。在prototype.js最初的實現中,$函式的定義為

var $ = function (id) {  
  return "string" == typeof id ? document.getElementById(id) : id;  
};

這基本對應於如下公式

e = $(id)   

這絕不僅僅是提供了一個聰明的函式名稱縮寫,更重要的是在概念層面上建立了文字id與DOM element之間的一一對應。在未有$之前,id與對應的element之間的距離十分遙遠,一般要將element快取到變數中,例如

var ea = docuement.getElementById('a');  
var eb = docuement.getElementById('b');

ea.style....  

但是使用$之後,卻隨處可見如下的寫法

$('header_'+id).style...  
$('body_'+id)....

id與element之間的距離似乎被消除了,可以非常緊密的交織在一起。
prototype.js後來擴充套件了$的含義,

function $() {  
  var elements = new Array();  
   
  for (var i = 0; i < arguments.length; i++) {  
      var element = arguments[i];  
      if (typeof element == 'string')  
        element = document.getElementById(element);  
   
      if (arguments.length == 1)  
        return element;  
   
      elements.push(element);  
  }  
   
  return elements;  
}

//這對應於公式

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

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

[o] = $(selector)  

這裡有三個增強

  • selector不再是單一的節點定位符,而是複雜的集合選擇符
  • 返回的元素不是原始的DOM節點,而是經過jQuery進一步增強的具有豐富行為的物件,可以啟動複雜的函式呼叫鏈。
  • $返回的包裝物件被造型為陣列形式,將集合操作自然的整合到呼叫鏈中。

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

$("<table><tbody><tr><td>...</td></tr></tbody></table>")....  

jQuery將根據傳入的html文字直接構造出一系列的DOM節點,並將其包裝為jQuery物件. 這在某種程度上可以看作是對selector的擴充套件: html內容描述本身就是一種唯一指定.

$(function{})這一功能就實在是讓人有些無語了, 它表示當document.ready的時候呼叫此回撥函式。真的,$是一個神奇的函式, 有任何問題,請$一下。
總結起來, $是從普通的DOM和文字描述世界到具有豐富物件行為的jQuery世界的躍遷通道。跨過了這道門,就來到了理想國。

4. 無定形的引數:專注表達而不是約束

弱型別語言既然頭上頂著個"弱"字, 總難免讓人有些先天不足的感覺. 在程式中缺乏型別約束, 是否真的是一種重大的缺憾? 在傳統的強型別語言中, 函式引數的型別,個數等都是由編譯器負責檢查的約束條件, 但這些約束仍然是遠遠不夠的. 一般應用程式中為了加強約束, 總會增加大量防禦性程式碼, 例如在C++中我們常用ASSERT, 而在java中也經常需要判斷引數值的範圍.

if (index < 0 || index >= size)  
    throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);
          

很顯然, 這些程式碼將導致程式中存在大量無功能的執行路徑, 即我們做了大量判斷, 程式碼執行到某個點, 系統丟擲異常, 大喊此路不通. 如果我們換一個思路, 既然已經做了某種判斷,能否利用這些判斷的結果來做些什麼呢? javascript是一種弱型別的語言,它是無法自動約束引數型別的, 那如果順勢而行,進一步弱化引數的形態, 將"弱"推進到一種極致, 在弱無可弱的時候, weak會不會成為標誌性的特點?
看一下jQuery中的事件繫結函式bind,

  • 一次繫結一個事件

    $("#my").bind("mouseover", function(){});

  • 一次繫結多個事件

    $("#my").bind("mouseover mouseout",function(){})

  • 換一個形式, 同樣繫結多個事件

    $("#my").bind({mouseover:function(){}, mouseout:function(){}});

  • 想給事件監聽器傳點引數

    $('#my').bind('click', {foo: "xxxx"}, function(event) { event.data.foo..})

  • 想給事件監聽器分個組

    $("#my").bind("click.myGroup″, function(){});

這個函式為什麼還沒有瘋掉???

就算是型別不確定, 在固定位置上的引數的意義總要是確定的吧? 退一萬步來說, 就算是引數位置不重要了,函式本身的意義應該是確定的吧? 但這是什麼?

value = o.val()
o.val(3)        

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

5. 鏈式操作: 線性化的逐步細化

jQuery早期最主要的賣點就是所謂的鏈式操作(chain).

$('#content') // 找到content元素  
.find('h3') // 選擇所有後代h3節點  
.eq(2)      // 過濾集合, 保留第三個元素  
    .html('改變第三個h3的文字')  
.end()      // 返回上一級的h3集合  
.eq(0)  
    .html('改變第一個h3的文字'); 

在一般的命令式語言中, 我們總需要在重重巢狀迴圈中過濾資料, 實際操作資料的程式碼與定位資料的程式碼糾纏在一起. 而jQuery採用先構造集合然後再應用函式於集合的方式實現兩種邏輯的解耦, 實現巢狀結構的線性化. 實際上, 我們並不需要藉助過程化的思想就可以很直觀的理解一個集合, 例如 $('div.my input:checked')可以看作是一種直接的描述,而不是對過程行為的跟蹤.
迴圈意味著我們的思維處於一種反覆迴繞的狀態, 而線性化之後則沿著一個方向直線前進, 極大減輕了思維負擔, 提高了程式碼的可組合性. 為了減少呼叫鏈的中斷, jQuery發明了一個絕妙的主意: jQuery包裝物件本身類似陣列(集合). 集合可以對映到新的集合, 集合可以限制到自己的子集合,呼叫的發起者是集合,返回結果也是集合,集合可以發生結構上的某種變化但它還是集合, 集合是某種概念上的不動點,這是從函式式語言中吸取的設計思想。集合操作是太常見的操作, 在java中我們很容易發現大量所謂的封裝函式其實就是在封裝一些集合遍歷操作, 而在jQuery中集合操作因為太直白而不需要封裝.
鏈式呼叫意味著我們始終擁有一個“當前”物件,所有的操作都是針對這一當前物件進行。這對應於如下公式

x += dx

呼叫鏈的每一步都是對當前物件的增量描述,是針對最終目標的逐步細化過程。Witrix平臺中對這一思想也有著廣泛的應用。特別是為了實現平臺機制與業務程式碼的融合,平臺會提供物件(容器)的預設內容,而業務程式碼可以在此基礎上進行逐步細化的修正,包括取消預設的設定等。
話說回來, 雖然表面上jQuery的鏈式呼叫很簡單, 內部實現的時候卻必須自己多寫一層迴圈, 因為編譯器並不知道"自動應用於集合中每個元素"這回事.

$.fn['someFunc'] = function(){  
    return this.each(function(){  
      jQuery.someFunc(this,...);  
    }  
}   

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提供了一個統一的全域性資料管理機制。

獲取資料

$('#my').data('myAttr')   

設定資料

$('#my').data('myAttr',3);  

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

<input id="my" data-my-attr="4" ... />   

通過 $('#my').data('myAttr')將可以讀取到HTML中設定的資料。

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

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除了為不同瀏覽器的事件模型建立了統一抽象之外,主要做了如下增強:

7.1增加了自定製事件(custom)機制.

事件的傳播機制與事件內容本身原則上是無關的, 因此自定製事件完全可以和瀏覽器內建事件通過同一條處理路徑, 採用同樣的監聽方式. 使用自定製事件可以增強程式碼的內聚性, 減少程式碼耦合. 例如如果沒有自定製事件, 關聯程式碼往往需要直接操作相關的物件

$('.switch, .clapper').click(function() {  
    var $light = $(this).parent().find('.lightbulb');  
    if ($light.hasClass('on')) {  
        $light.removeClass('on').addClass('off');  
    } else {  
        $light.removeClass('off').addClass('on');  
    }  
});  

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

$('.switch, .clapper').click(function() {  
  $(this).parent().find('.lightbulb').trigger('changeState');  
});  
7.2. 增加了對動態建立節點的事件監聽. bind函式只能將監聽函式註冊到已經存在的DOM節點上. 例如
$('li.trigger').bind('click',function(){}}  

如果呼叫bind之後,新建了另一個li節點,則該節點的click事件不會被監聽.
jQuery的delegate機制可以將監聽函式註冊到父節點上, 子節點上觸發的事件會根據selector被自動派發到相應的handlerFn上. 這樣一來現在註冊就可以監聽未來建立的節點.

$('#myList').delegate('li.trigger', 'click', handlerFn);  

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

$('li.trigger’).on('click', handlerFn);  // 相當於bind  
$('#myList’).on('click', 'li.trigger', handlerFn);  // 相當於delegate  

8. 動畫佇列:全域性時鐘協調

拋開jQuery的實現不談, 先考慮一下如果我們要實現介面上的動畫效果, 到底需要做些什麼? 比如我們希望將一個div的寬度在1秒鐘之內從100px增加到200px. 很容易想見, 在一段時間內我們需要不時的去調整一下div的寬度, [同時]我們還需要執行其他程式碼. 與一般的函式呼叫不同的是, 發出動畫指令之後, 我們不能期待立刻得到想要的結果, 而且我們不能原地等待結果的到來. 動畫的複雜性就在於:一次性表達之後要在一段時間內執行,而且有多條邏輯上的執行路徑要同時展開, 如何協調?
偉大的艾薩克.牛頓爵士在《自然哲學的數學原理》中寫道:"絕對的、真正的和數學的時間自身在流逝著". 所有的事件可以在時間軸上對齊, 這就是它們內在的協調性. 因此為了從步驟A1執行到A5, 同時將步驟B1執行到B5, 我們只需要在t1時刻執行[A1, B1], 在t2時刻執行[A2,B2], 依此類推.

t1 | t2 | t3 | t4 | t5 ...  
A1 | A2 | A3 | A4 | A5 ...  
B1 | B2 | B3 | B4 | B5 ...  

具體的一種實現形式可以是

A. 對每個動畫, 將其分裝為一個Animation物件, 內部分成多個步驟.
animation = new Animation(div,"width",100,200,1000);
B. 在全域性管理器中註冊動畫物件
timerFuncs.add(animation);
C. 在全域性時鐘的每一個觸發時刻, 將每個註冊的執行序列推進一步, 如果已經結束, 則從全域性管理器中刪除.
for each animation in timerFuncs  
if(!animation.doOneStep())  
  timerFuncs.remove(animation)  

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

$('input')  
  .animate({left:'+=200px',top:'300'},2000)  
  .animate({left:'-=200px',top:20},1000)  
  .queue(function(){  
    // 這裡dequeue將首先執行佇列中的後一個函式,因此alert("y")  
    $(this).dequeue();  
    alert('x');  
   })  
  .queue(function(){  
     alert("y");  
     // 如果不主動dequeue, 佇列執行就中斷了,不會自動繼續下去.  
     $(this).dequeue();  
   });  
  • 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模式.

futureResult = doSomething();  
...  
realResult = futureResult.get();  

發出函式呼叫僅僅意味著一件事情發生過, 並不必然意味著呼叫者需要了解事情最終的結果. 函式立刻返回的只是一個將在未來兌現的承諾(Future型別), 實際上也就是某種控制代碼. 控制代碼被傳來傳去, 中間轉手的程式碼對實際結果是什麼,是否已經返回漠不關心. 直到一段程式碼需要依賴呼叫返回的結果, 因此它開啟future, 查看了一下. 如果實際結果已經返回, 則future.get()立刻返回實際結果, 否則將會阻塞當前的執行路徑, 直到結果返回為止. 此後再呼叫future.get()總是立刻返回, 因為因果關係已經被建立, [結果返回]這一事件必然在此之前發生, 不會再發生變化.
future模式一般是外部物件主動檢視future的返回值, 而promise模式則是由外部物件在promise上註冊回撥函式.

function getData(){  
 return $.get('/foo/').done(function(){  
    console.log('Fires after the AJAX request succeeds');  
 }).fail(function(){  
    console.log('Fires after the AJAX request fails');  
 });  
}  

function showDiv(){  
  var dfd = $.Deferred();  
  $('#foo').fadeIn( 1000, dfd.resolve );  
  return dfd.promise();  
}  

$.when( getData(), showDiv() )  
  .then(function( ajaxResult, ignoreResultFromShowDiv ){  
      console.log('Fires after BOTH showDiv() AND the AJAX request succeed!');  
      // 'ajaxResult' is the server’s response  
  });  

jQuery引入Deferred結構, 根據promise模式對ajax, queue, document.ready等進行了重構, 統一了非同步執行機制. then(onDone, onFail)將向promise中追加回調函式, 如果呼叫成功完成(resolve), 則回撥函式onDone將被執行, 而如果呼叫失敗(reject), 則onFail將被執行. when可以等待在多個promise物件上. promise巧妙的地方是非同步執行已經開始之後甚至已經結束之後,仍然可以註冊回撥函式

someObj.done(callback).sendRequest() vs. someObj.sendRequest().done(callback)

callback函式在發出非同步呼叫之前註冊或者在發出非同步呼叫之後註冊是完全等價的, 這揭示出程式表達永遠不是完全精確的, 總存在著內在的變化維度. 如果能有效利用這一內在的可變性, 則可以極大提升併發程式的效能.
promise模式的具體實現很簡單. jQuery._Deferred定義了一個函式佇列,它的作用有以下幾點:

A. 儲存回撥函式。
B. 在resolve或者reject的時刻把儲存著的函式全部執行掉。
C. 已經執行之後, 再增加的函式會被立刻執行。

一些專門面向分散式計算或者平行計算的語言會在語言級別內建promise模式, 比如E語言.

def carPromise := carMaker <- produce("Mercedes");  
def temperaturePromise := carPromise <- getEngineTemperature()  
...  
when (temperaturePromise) -> done(temperature) {  
  println(`The temperature of the car engine is: $temperature`)  
} catch e {  
  println(`Could not get engine temperature, error: $e`)  
}  

在E語言中, <-是eventually運算子, 表示最終會執行, 但不一定是現在. 而普通的car.moveTo(2,3)表示立刻執行得到結果. 編譯器負責識別所有的promise依賴, 並自動實現排程.

10. extend: 繼承不是必須的

js是基於原型的語言, 並沒有內建的繼承機制, 這一直讓很多深受傳統面向物件教育的同學們耿耿於懷. 但繼承一定是必須的嗎? 它到底能夠給我們帶來什麼? 最純樸的回答是: 程式碼重用. 那麼, 我們首先來分析一下繼承作為程式碼重用手段的潛力.
曾經有個概念叫做"多重繼承", 它是繼承概念的超級賽亞人版, 很遺憾後來被診斷為存在著先天缺陷, 以致於出現了一種對於繼承概念的解讀: 繼承就是"is a"關係, 一個派生物件"is a"很多基類, 必然會出現精神分裂, 所以多重繼承是不好的.

class A{ public: void f(){ f in A } }  
class B{ public: void f(){ f in B } }  
class D: public A, B{} 

如果D類從A,B兩個基類繼承, 而A和B類中都實現了同一個函式f, 那麼D類中的f到底是A中的f還是B中的f, 抑或是A中的f+B中的f呢? 這一困境的出現實際上源於D的基類A和B是並列關係, 它們滿足交換律和結合律, 畢竟,在概念層面上我們可能難以認可兩個任意概念之間會出現從屬關係. 但如果我們放鬆一些概念層面的要求, 更多的從操作層面考慮一下程式碼重用問題, 可以簡單的認為B在A的基礎上進行操作, 那麼就可以得到一個線性化的結果. 也就是說, 放棄A和B之間的交換律只保留結合律, extends A, B 與 extends B,A 會是兩個不同的結果, 不再存在詮釋上的二義性. scala語言中的所謂trait(特性)機制實際上採用的就是這一策略.
面向物件技術發明很久之後, 出現了所謂的面向方面程式設計(AOP), 它與OOP不同, 是程式碼結構空間中的定位與修改技術. AOP的眼中只有類與方法, 不知道什麼叫做意義. AOP也提供了一種類似多重繼承的程式碼重用手段, 那就是mixin. 物件被看作是可以被開啟,然後任意修改的Map, 一組成員變數與方法就被直接注射到物件體內, 直接改變了它的行為.
prototype.js庫引入了extend函式,

Object.extend = function(destination, source) {  
  for (var property in source) {  
    destination[property] = source[property];  
  }  
  return destination;  
}  

就是Map之間的一個覆蓋運算, 但很管用, 在jQuery庫中也得到了延用. 這個操作類似於mixin, 在jQuery中是程式碼重用的主要技術手段---沒有繼承也沒什麼大不了的.

11. 名稱對映: 一切都是資料

程式碼好不好, 迴圈判斷必須少. 迴圈和判斷語句是程式的基本組成部分, 但是優良的程式碼庫中卻往往找不到它們的蹤影, 因為這些語句的交織會模糊系統的邏輯主線, 使我們的思想迷失在疲於奔命的程式碼追蹤中. jQuery本身通過each, extend等函式已經極大減少了對迴圈語句的需求, 對於判斷語句, 則主要是通過對映表來處理. 例如, jQuery的val()函式需要針對不同標籤進行不同的處理, 因此定義一個以tagName為key的函式對映表

valHooks: { option: {get:function(){}}}  

這樣在程式中就不需要到處寫

if(elm.tagName == 'OPTION'){  
  return ...;  
}else if(elm.tagName == 'TEXTAREA'){  
  return ...;  
}   

可以統一處理

(valHooks[elm.tagName.toLowerCase()] || defaultHandler).get(elm);  

對映表將函式作為普通資料來管理, 在動態語言中有著廣泛的應用. 特別是, 物件本身就是函式和變數的容器, 可以被看作是對映表. jQuery中大量使用的一個技巧就是利用名稱對映來動態生成程式碼, 形成一種類似模板的機制. 例如為了實現myWidth和myHeight兩個非常類似的函式, 我們不需要

jQuery.fn.myWidth = function(){  
    return parseInt(this.style.width,10) + 10;  
}  

jQuery.fn.myHeight = function(){  
    return parseInt(this.style.height,10) + 10;  
}

而可以選擇動態生成

jQuery.each(['Width','Height'],function(name){  
  jQuery.fn['my'+name] = function(){  
    return parseInt(this.style[name.toLowerCase()],10) + 10;  
  }  
});   

12. 外掛機制:其實我很簡單

jQuery所謂的外掛其實就是$.fn上增加的函式, 那這個fn是什麼東西?

(function(window,undefined){  
  // 內部又有一個包裝  
  var jQuery = (function() {  
    var jQuery = function( selector, context ) {  
          return new jQuery.fn.init( selector, context, rootjQuery );  
      }  
     ....  
    // fn實際就是prototype的簡寫  
    jQuery.fn = jQuery.prototype = {  
        constructor: jQuery,  
        init: function( selector, context, rootjQuery ) {...  }  
    }  
   
    // 呼叫jQuery()就是相當於new init(), 而init的prototype就是jQuery的prototype  
    jQuery.fn.init.prototype = jQuery.fn;  
   
    // 這裡返回的jQuery物件只具備最基本的功能, 下面就是一系列的extend  
    return jQuery;  
  })();   
  ...  
   // 將jQuery暴露為全域性物件  
  window.jQuery = window.$ = jQuery;  
})(window);   

顯然, $.fn其實就是jQuery.prototype的簡寫.

無狀態的外掛僅僅就是一個函式, 非常簡單.

// 定義外掛  
(function($){  
    $.fn.hoverClass = function(c) {  
        return this.hover(  
            function() { $(this).toggleClass(c); }  
        );  
    };  
})(jQuery);  

// 使用外掛  
$('li').hoverClass('hover');  

對於比較複雜的外掛開發, jQuery UI提供了一個widget工廠機制,

$.widget("ui.dialog", {  
  options: {  
       autoOpen: true,...  
    },  
    _create: function(){ ... },  
    _init: function() {  
       if ( this.options.autoOpen ) {  
           this.open();  
       }  
    },  
    _setOption: function(key, value){ ... }  
    destroy: function(){ ... }  
});  

呼叫 $('#dlg').dialog(options)時, 實際執行的程式碼基本如下所示:

this.each(function() {  
      var instance = $.data( this, "dialog" );  
      if ( instance ) {  
          instance.option( options || {} )._init();  
      } else {  
          $.data( this, "dialog", new $.ui.dialog( options, this ) );  
      }  
}

可以看出, 第一次呼叫$('#dlg').dialog()函式時會建立視窗物件例項,並儲存在data中, 此時會呼叫_create()和_init()函式, 而如果不是第一次呼叫, 則是在已經存在的物件例項上呼叫_init()方法. 多次呼叫$('#dlg').dialog()並不會建立多個例項.

over,下篇寫如何編寫jquery外掛。