許多的語言,為了將非同步模式處理得更像平常的順序,都包含一種有趣的方案庫,它們被稱之為promises,deferreds,或futures。 JavaScript的promises ,可以促進關注點分離,以取代緊密耦合的介面。 本文講的是基於Promises/A 標準的JavaScript promises。 [http://wiki.commonjs.org/wiki/Promises/A]
Promise的用例:
JavaScript promise是一個承諾將在未來傳回值的物件。是具有良好定義的行為的資料對象。 promise有三種可能的狀態:
一個已經拒絕或完成的承諾屬於已經解決的。一個承諾只能從待定狀態變成已經解決的狀態。之後,承諾的狀態就不變了。承諾可以在它對應的處理完成之後很久還存在。也就是說,我們可以多次取得處理結果。我們透過呼叫promise.then()來取得結果,這個函數一直到承諾對應的處理結束才會回傳。我們可以靈活的串連起一堆承諾。這些串連起來的「then」函數應該回傳一個新的承諾或是最早的那個承諾。
透過這個樣式,我們可以像寫同步程式碼一樣來寫非同步程式碼。主要是透過組合承諾來實現:
為什麼要這麼麻煩?只用基本的回呼函數不行嗎?
回呼函數的問題
回呼函數適合簡單的重複事件,例如根據點擊來讓一個表單有效,或儲存一個REST呼叫的結果。回呼函數也會使程式碼形成一個鏈,一個回呼函數呼叫一個REST函數,並為REST函數設定一個新的回呼函數,這個新的回呼函數再呼叫另一個REST函數,依此類推。程式碼的橫向增長大於縱向的增長。回呼函數看起來很簡單,直到我們需要一個結果,而且是立刻就要,馬上就用在下一行的計算中。
'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();
我使用timeout來模擬非同步操作。管理異常的方法是痛苦的,很容易玩漏下游行為。當我們編寫回調,那麼程式碼組織變得混亂。圖2顯示了一個模擬驗證流可以運行在NodeJS REPL。在下一節,我們將從pyramid-of-doom模式遷移到一個連續的promise。
Figure
'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();
在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
其中採用了kew promise函式庫。 Q庫同樣適用。要使用該函式庫,請先使用npm將kew函式庫匯入到NodeJS,然後載入程式碼到NodeJS REPL。
Figure
'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();
輸出和使用巢狀回呼時相同:
$ 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所有的代码是可修改的。我故意操作随意动作。你可以试几次得到相反的结果。它是可以直接扩展到多个promise链, 就像前面NodeJS例子。
并行 Promises
考虑一个异步操作喂养另一个异步操作。让后者包括三个并行异步行为,反过来,喂最后一个行动。只有当所有平行的子请求通过才能通过。这是灵感来自偶遇一打MongoDB操作。有些是合格的并行操作。我实现了promises的流流程图。
我们怎么会模拟那些在该图中心行的并行promises?关键是,最大的promise库有一个全功能,它产生一个包含一组子promises的父promie。当所有的子promises通过,父promise通过。如果有一个子promise拒绝,父promise拒绝。
让十个并行的promises每个都包含一个文字promise。只有当十个子类通过或如果任何子类拒绝,最后的then方法才能完成。
Figure
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( . . .
下面的地址, http://jsfiddle.net/mauget/XKCy2/,针对JSFiddle在浏览器中运行十个并行promises,随机的拒绝或通过。这里有完整的代码用于检查和变化if条件。重新运行,直到你得到一个相反的完成。
孕育 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 开始。