JavaScript Promises 相當酷

jopen 10年前發布 | 23K 次閱讀 Promises JavaScript開發

“And when I promise something, I never ever break that promise. Never.” ― Rapunzel

許多的語言,為了將異步模式處理得更像平常的順序,都包含一種有趣的方案庫,它們被稱之為promises,deferreds,或者futures。JavaScript的promises ,可以促進關注點分離,以代替緊密耦合的接口。 本文講的是基于Promises/A 標準的JavaScript promises。[http://wiki.commonjs.org/wiki/Promises/A]

Promise的用例:

  • 執行規則 

    </li>

  • 多個遠程驗證 

    </li>

  • 超時處理 

    </li>

  • 遠程數據請求 

    </li>

  • 動畫 

    </li>

  • 將事件邏輯從應用邏輯中解耦 

    </li>

  • 消除回調函數的恐怖三角

    </li>

  • 控制并行的異步操作

    </li> </ul>

    JavaScript promise是一個承諾將在未來返回值的對象。是具有良好定義的行為的數據對象。promise有三種可能的狀態:

    1. Pending(待定)

      </li>

    2. Rejected(拒絕)

      </li>

    3. Resolved(已完成)

      </li> </ol>

      一個已經拒絕或者完成的承諾屬于已經解決的。一個承諾只能從待定狀態變成已經解決的狀態。之后,承諾的狀態就不變了。承諾可以在它對應的處理完成之后很久還存在。也就是說,我們可以多次取得處理結果。我們通過調用promise.then()來取得結果,這個函數一直到承諾對應的處理結束才會返回。我們可以靈活的串聯起一堆承諾。這些串聯起來的“then”函數應該返回一個新的承諾或者最早的那個承諾。
      通過這個樣式,我們可以像寫同步代碼一樣來寫非同步代碼。主要是通過組合承諾來實現:

      • 堆棧式任務:多處散落在代碼中的,對應同一個承諾。

        </li> </ul>

        • 并行任務:多個承諾返回同一個承諾。

          </li> </ul>

          • 串行任務:一個承諾,然后接著執行另一個承諾。

            </li> </ul>

            • 上面幾種的組合。

              </li> </ul>

              為什么要這么麻煩?只用基本的回調函數不行嗎?

              回調函數的問題

              回調函數適合簡單的重復性事件,例如根據點擊來讓一個表單有效,或者保存一個REST調用的結果。回調函數還會使代碼形成一個鏈,一個回調函數調用一個REST函數,并為REST函數設置一個新的回調函數,這個新的回調函數再調用另一個REST函數,依此類推。這就形成了一個如圖1中的毀滅金字塔。代碼的橫向增長大于縱向的增長。回調函數看起來很簡單,直到我們需要一個結果,而且是立刻就要,馬上就用在下一行的計算中。
              JavaScript Promises 相當酷
              圖1:毀滅金字塔

              'use strict';
              var i = 0;
              function log(data) {console.log('%d %s', ++i, data); };

              function validate() {    log("Wait for it ...");    // Sequence of four Long-running async activities    setTimeout(function () {       log('result first');       setTimeout(function () {          log('result second');          setTimeout(function () {             log('result third');             setTimeout(function () {                log('result fourth')             }, 1000);          }, 1000);       }, 1000);    }, 1000);

              }; validate();</pre>

              在圖1中,我使用timeout來模擬異步操作。管理異常的方法是痛苦的,很容易玩漏下游行為。當我們編寫回調,那么代碼組織變得混亂。圖2顯示了一個模擬驗證流可以運行在NodeJS REPL。在下一節,我們將從pyramid-of-doom模式遷移到一個連續的promise。

              'use strict';
              var i = 0;
              function log(data) {console.log('%d %s', ++i, data); };

              // Asynchronous fn executes a callback result fn function async(arg, callBack) {    setTimeout(function(){       log('result ' + arg);       callBack();    }, 1000); };

              function validate() {    log("Wait for it ...");    // Sequence of four Long-running async activities    async('first', function () {       async('second',function () {          async('third', function () {             async('fourth', function () {});          });       });    }); }; validate();</pre>

              在NodeJS REPL執行的結果

              $ node scripts/examp2b.js
              1 Wait for it ...
              2 result first
              3 result second
              4 result third
              5 result fourth
              $

              我曾經遇到一個AngularJS動態驗證的情況,根據對應表的值,動態的限制表單項的值。限制項的有效值范圍被定義在REST服務上。

              我寫了一個調度器,根據請求的值,去操作函數棧,以避免回調嵌套。調度器從棧中彈出函數并執行。函數的回調會在結束時重新調用調度器,直到棧被清空。每次回調都記錄所有從遠程驗證調用返回的驗證錯誤。

              我認為我寫的玩意兒是一種反模式。如果我用Angular的$http調用提供的promise,在整個驗證過程中我的思維會更近似線性形式,就像同步編程。平展的promise鏈是可讀的。繼續...

              使用Promises

              圖3顯示了我將驗證改寫成promise鏈的樣子。其中采用了kew promise庫。Q庫同樣適用。要使用該庫,首先使用npm將kew庫導入到NodeJS,然后加載代碼到NodeJS REPL。

              'use strict';
              var Q = require('kew');
              var i = 0;

              function log(data) {console.log('%d %s', ++i, data); };

              // Asynchronous fn returns a promise function async(arg) { var deferred = Q.defer(); setTimeout(function () { deferred.resolve('result ' + arg);\ }, 1000); return deferred.promise; };

              // Flattened promise chain function validate() { log("Wait for it ..."); async('first').then(function(resp){ log(resp); return async('second'); }) .then(function(resp){ log(resp); return async('third') }) .then(function(resp){ log(resp); return async('fourth'); }) .then(function(resp){ log(resp); }).fail(log); }; validate();</pre>

              輸出和使用嵌套回調時相同:

              $ node scripts/examp2-pflat.js
              1 Wait for it ...
              2 result first
              3 result second
              4 result third
              5 result fourth
              $

              該代碼稍微“長高”了,但我認為更易于理解和修改。更易于加上適當的錯誤處理。在鏈的末尾調用fail用于捕獲鏈中錯誤,但我也可以在任何一個then里面提供一個reject的處理函數做相應的處理。

              服務器 或 瀏覽器

              Promises在瀏覽器中就像在NodeJS服務器中一樣有效。下面的地址, http://jsfiddle.net/mauget/DnQDx/,指向JSFiddle的一個展示如何使用一個promise的web頁面。 JSFiddle所有的代碼是可修改的。瀏覽器輸出的一個變化如圖4所示。我故意操作隨意動作。你可以試幾次得到相反的結果。它是可以直接擴展到多個promise鏈, 就像前面NodeJS例子。

              JavaScript Promises 相當酷

              圖4.單個promise

              并行 Promises

              考慮一個異步操作喂養另一個異步操作。讓后者包括三個并行異步行為,反過來,喂最后一個行動。只有當所有平行的子請求通過才能通過。如圖5所示。這是靈感來自偶遇一打MongoDB操作。有些是合格的并行操作。我實現了promises的流流程圖。

              JavaScript Promises 相當酷

              圖5:異步操作的結構

              我們怎么會模擬那些在該圖中心行的并行promises?關鍵是,最大的promise庫有一個全功能,它產生一個包含一組子promises的父promie。當所有的子promises通過,父promise通過。如果有一個子promise拒絕,父promise拒絕。

              圖6顯示了一個代碼片段,讓十個并行的promises每個都包含一個文字promise。只有當十個子類通過或如果任何子類拒絕,最后的then方法才能完成。

               var promiseVals = ['To ', 'be, ', 'or ',
                      'not ', 'to ', 'be, ', 'that ',
                      'is ', 'the ', 'question.'];

              var startParallelActions = function (){
                  var promises = [];
              
                  // Make an asynchronous action from each literal
                  promiseVals.forEach(function(value){
                      promises.push(makeAPromise(value));
                  });
              
                  // Consolidate all promises into a promise of promises
                  return Q.all(promises);
              };
              
              startParallelActions ().then( . . .</pre> <p></p>
              

              下面的地址, http://jsfiddle.net/mauget/XKCy2/,針對JSFiddle在瀏覽器中運行十個并行promises,隨機的拒絕或通過。這里有完整的代碼用于檢查和變化if條件。重新運行,直到你得到一個相反的完成。圖7顯示了積極的結果。

              JavaScript Promises 相當酷

              圖7:JSFiddle并行promises樣例

              孕育 Promise

              許多api返回的promise都有一個then函數——他們是thenable。通常我只是通過then處理thenable函數的結果。然而,$q,mpromise,和kew庫擁有同樣的API用于創建,拒絕,或者通過promise。這里有API文檔鏈接到每個庫的引用部分。我通常不需要構造一個promise,除了本文中的包裝promise的未知描述和timeout函數。請參考哪些我創建的promises。

              Promise庫互操作

              大多數JavaScript promise庫在then級別進行互操作。你可以從一個外部的promise創建一個promise,因為promise可以包裝任何類型的值。then可以支持跨庫工作。除了then,其他的promise函數則可能不同。如果你需要一個你的庫不包含的函數,你可以將一個基于你的庫的promise包裝到一個新的,基于含有你所需函數的庫創建的promise里面。例如,JQuery的promise有時為人所詬病。那么你可以將其包裝到Q,$q,mpromise,或者kew庫的promise中進行操作。

              結語

              現在我寫了這篇文章,而一年前我卻是猶豫要不要擁抱promise的那個。我只是單純地想完成一項工作。 我不想學習新的API,或是打破我原來的代碼(因為誤解了promise)。我曾經如此錯誤地認為!當我下了一點注時,就輕易就贏得了可喜的成果。

              在這篇文章中,我已經簡單給出了一個單一的promise,promise鏈,和一個并行的promise的promise的的例子。 Promises不難使用。如果我可以使用它們,任何人都可以。 要查看完整的概念,我支持你點擊專家寫的參考指南。從Promises/A 的參考開始,從事實上的標準JavaScript的Promise 開始。

              如果你還沒有直接使用的promise,試一下。下定決心:你會有一個不錯的體驗。我保證!

              – Lou Mauget, asktheteam@keyholesoftware.com

              參考鏈接

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!