jQuery 中的編程範式(中)

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語法表達力的最後一點剩餘價值.

  1. $(‘input’)
  2.   .animate({left:’+=200px’,top:’300′},2000)
  3.   .animate({left:’-=200px’,top:20},1000)
  4.   .queue(function(){
  5.     // 這裏dequeue將首先執行隊列中的後一個函數,因此alert(“y”)
  6.     $(this).dequeue();
  7.     alert(‘x’);
  8.    })
  9.   .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。

  1. 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. realResult = futureResult.get();
複製代碼

        發出函數調用僅僅意味着一件事情發生過, 並不必然意味着調用者需要了解事情最終的結果。函數立刻返回的只是一個將在未來兌現的承諾(Future類型), 實際上也就是某種句柄。句柄被傳來傳去, 中間轉手的代碼對實際結果是什麼,是否已經返回漠不關心。直到一段代碼需要依賴調用返回的結果, 因此它打開future, 查看了一下。如果實際結果已經返回, 則future.get()立刻返回實際結果, 否則將會阻塞當前的執行路徑, 直到結果返回爲止。此後再調用future.get()總是立刻返回, 因爲因果關係已經被建立, [結果返回]這一事件必然在此之前發生, 不會再發生變化。

        future模式一般是外部對象主動查看future的返回值, 而promise模式則是由外部對象在promise上註冊回調函數。

  1. function getData(){
  2. return $.get(‘/foo/’).done(function(){
  3.     console.log(‘Fires after the AJAX request succeeds’);
  4. }).fail(function(){
  5.     console.log(‘Fires after the AJAX request fails’);
  6. });
  7. }

  8. function showDiv(){
  9.   var dfd = $.Deferred();
  10.   $(‘#foo’).fadeIn( 1000, dfd.resolve );
  11.   return dfd.promise();
  12. }

  13. $.when( getData(), showDiv() )
  14.   .then(function( ajaxResult, ignoreResultFromShowDiv ){
  15.       console.log(‘Fires after BOTH showDiv() AND the AJAX request succeed!’);
  16.       // ’ajaxResult’ is the server’s response
  17.   });
複製代碼

        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語言。

  1. def carPromise := carMaker <- produce(“Mercedes”);
  2. def temperaturePromise := carPromise <- getEngineTemperature()

  3. when (temperaturePromise) -> done(temperature) {
  4.   println(`The temperature of the car engine is: $temperature`)
  5. } catch e {
  6.   println(`Could not get engine temperature, error: $e`)
  7. }
複製代碼

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

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