首頁 >web前端 >js教程 >jQuery中的程式設計範式詳解_jquery

jQuery中的程式設計範式詳解_jquery

WBOY
WBOY原創
2016-05-16 16:27:011483瀏覽

本文詳細分析了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是對外暴露的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 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         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. $傳回的包裝物件被造型為陣列形式,將集合運算自然的整合到呼叫鏈中。

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

複製程式碼 程式碼如下:
$("
...
")....

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

$(function{})這一功能就實在是讓人有些無語了, 它表示當document.ready的時候調用此回調函數。真的,$是一個神奇的函數, 有任何問題,請$一下。

總結起來, $是從普通的DOM和文字描述世界到具有豐富物件行為的jQuery世界的躍遷通道。跨過了這道門,就來到了理想國。
  
4. 無定形的參數:專​​注表達而非限制

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

複製程式碼 程式碼如下:
if (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提供了一個統一的全域資料管理機制。

取得資料:

複製程式碼 程式碼如下:
$('#myAttr ')

設定資料:
複製程式碼 程式碼如下:
$('#my').data('myAttr',3>
$('#my').data('myAttr',3 );

這機制自然融合了對HTML5的data屬性的處理 代碼如下:


透過 $('#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      def temperaturePromise := carPromise      ...
     when (temperaturePromise) -> done(temperature) {
       println(`The temperature of the car engine is: $temperature`)
     } catch e {
       println(`Could not get engine temperature, error: $e`)
     }

在E語言中,  
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) {
      // ...
  }
 

但是隨著瀏覽器市場的競爭升級, 競爭對手之間的互相模仿和偽裝導致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版本據說要做大的變革, 不知是要與歷史決裂, 放棄相容性, 還是繼續掙扎, 在夾縫中求生.

希望本文所述對大家的jQuery程式設計有所幫助。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn