jQuery中的編程範式

     瀏覽器前端編程的面貌自2005年以來已經發生了深刻的變化,這並不簡單的意味着出現了大量功能豐富的基礎庫,使得我們可以更加方便的編寫業務代碼,更重要的是我們看待前端技術的觀念發生了重大轉變,明確意識到了如何以前端特有的方式釋放程序員的生產力。本文將結合jQuery源碼的實現原理,對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作爲公共代碼基也就缺乏存在的理由。

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)

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

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

$("<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,
   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. 這個函數爲什麼還沒有瘋掉???
  
   就算是類型不確定, 在固定位置上的參數的意義總要是確定的吧? 退一萬步來說, 就算是參數位置不重要了,函數本身的意義應該是確定的吧? 但這是什麼?

     取值 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除了爲不同瀏覽器的事件模型建立了統一抽象之外,主要做了如下增強:
  A. 增加了自定製事件(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');
  });


  B. 增加了對動態創建節點的事件監聽. 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()並不會創建多個實例.

13. browser sniffer vs. feature detection
  瀏覽器嗅探(browser sniffer)曾經是很流行的技術, 比如早期的jQuery中

  jQuery.browser = {
        version:(userAgent.match(/.+(?:rv|it|ra|ie)[/: ]([d.]+)/) || [0,'0'])[1],
        safari:/webkit/.test(userAgent),
        opera:/opera/.test(userAgent),
        msie:/msie/.test(userAgent) && !/opera/.test(userAgent),
        mozilla:/mozilla/.test(userAgent) && !/(compatible|webkit)/.test(userAgent)
  };

  在具體代碼中可以針對不同的瀏覽器作出不同的處理
  if($.browser.msie) {
      // do something
  } else if($.browser.opera) {
      // ...
  }

  if($.browser.msie) {
      // do something
  } else if($.browser.opera) {
      // ...
  } 

  但是隨着瀏覽器市場的競爭升級, 競爭對手之間的互相模仿和僞裝導致userAgent一片混亂, 加上Chrome的誕生, Safari的崛起, IE也開始加速向標準靠攏, sniffer已經起不到積極的作用. 特性檢測(feature detection)作爲更細粒度, 更具體的檢測手段, 逐漸成爲處理瀏覽器兼容性的主流方式.

  jQuery.support = {
        // IE strips leading whitespace when .innerHTML is used
        leadingWhitespace: ( div.firstChild.nodeType === 3 ),
        ...
    }

    只基於實際看見的,而不是曾經知道的, 這樣更容易做到兼容未來.

14. Prototype vs. jQuery
  prototype.js是一個立意高遠的庫, 它的目標是提供一種新的使用體驗,參照Ruby從語言級別對javascript進行改造,並最終真的極大改變了js的面貌。$, extends, each, bind...這些耳熟能詳的概念都是prototype.js引入到js領域的. 它肆無忌憚的在window全局名字空間中增加各種概念, 大有誰先佔坑誰有理, 捨我其誰的氣勢. 而jQuery則扣扣索索, 抱着比較實用化的理念, 目標僅僅是write less, do more而已. 
  不過等待激進的理想主義者的命運往往都是壯志未酬身先死. 當prototype.js標誌性的bind函數等被吸收到ECMAScript標準中時, 便註定了它的沒落. 到處修改原生對象的prototype, 這是prototype.js的獨門祕技, 也是它的死穴. 特別是當它試圖模仿jQuery, 通過Element.extend(element)返回增強對象的時候, 算是徹底被jQuery給帶到溝裏去了. prototype.js與jQuery不同, 它總是直接修改原生對象的prototype, 而瀏覽器卻是充滿bug, 謊言, 歷史包袱並夾雜着商業陰謀的領域, 在原生對象層面解決問題註定是一場悲劇. 性能問題, 名字衝突, 兼容性問題等等都是一個幫助庫的能力所無法解決的. Prototype.js的2.0版本據說要做大的變革, 不知是要與歷史決裂, 放棄兼容性, 還是繼續掙扎, 在夾縫中求生.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章