若是你寫過任何數量至關的JavaScript,這就不是什麼祕密:異步編程是一種必須的技能。管理異步的主要機制曾經是函數回調。
然而,ES6增長了一種新特性:Promise,來幫助你解決僅使用回調來管理異步的重大缺陷。另外,咱們能夠重溫generator,來看看一種將二者組合的模式,它是JavaScript中異步流程控制編程向前邁出的重要一步。ajax
讓咱們辨明一些誤解:Promise不是回調的替代品。Promise提供了一種可信的中介機制 —— 也就是,在你的調用代碼和將要執行任務的異步代碼之間 —— 來管理回調。
另外一種考慮Promise的方式是做爲一種事件監聽器,你能夠在它上面註冊監聽一個通知你任務什麼時候完成的事件。它是一個僅被觸發一次的事件,但無論怎樣能夠被看做是一個事件。
Promise能夠被連接在一塊兒,它們能夠是一系列順序的、異步完成的步驟。與all(..)方法(用經典的術語將,叫「門」)和race(..)方法(用經典的術語將,叫「閂」)這樣的高級抽象一塊兒,promise鏈能夠提供一種異步流程控制的機制。
還有另一種概念化Promise的方式是,將它看做一個 將來值,一個與時間無關的值的容器。不管底層的值是不是最終值,這種容器均可以被一樣地推理。觀測一個Promise的解析會在這個值準備好的時候將它抽取出來。換言之,一個Promise被認爲是一個同步函數返回值的異步版本。
一個Promise只可能擁有兩種解析結果:完成或拒絕,並帶有一個可選的信號值。若是一個Promise被完成,這個最終值稱爲一個完成值。若是它被拒絕,這個最終值稱爲理由(也就是「拒絕的理由」)。Promise只可能被解析(完成或拒絕)一次。任何其餘的完成或拒絕的嘗試都會被簡單地忽略,一旦一個Promise被解析,它就成爲一個不可被改變的值(immutable)。
顯然,有幾種不一樣的方式能夠來考慮一個Promise是什麼。沒有一個角度就它自身來講是徹底充分的,可是每個角度都提供了總體的一個方面。這其中的要點是,它們爲僅使用回調的異步提供了一個重大的改進,也就是它們提供了順序、可預測性、以及可信性。編程
要構建一個promise實例,可使用Promise(..)構造器:數組
var promise = new Promise( function pr(resolve,reject){ resolve() } );
Promise(..)構造器接收一個單獨的函數(pr(..)),它被當即調用並以參數值的形式收到兩個控制函數,一般被命名爲resolve(..)和reject(..)。它們被這樣使用:promise
這裏是你一般如何使用一個promise來重構一個依賴於回調的函數調用。假定你始於使用一個ajax(..)工具,它期預期要調用一個錯誤優先風格的回調:瀏覽器
function ajax(url,cb) { // 發起請求,最終調用 `cb(..)` } // .. ajax( "http://some.url.1", function handler(err,contents){ if (err) { // 處理ajax錯誤 } else { // 處理成功的`contents` } } );
function ajax(url) { return new Promise( function pr(resolve,reject){ // 發起請求,最終不是調用 `resolve(..)` 就是調用 `reject(..)` } ); } // .. ajax( "http://some.url.1" ) .then( function fulfilled(contents){ // 處理成功的 `contents` }, function rejected(reason){ // 處理ajax的錯誤reason } );
Promise擁有一個方法then(..),它接收一個或兩個回調函數。第一個函數(若是存在的話)被看做是promise被成功地完成時要調用的處理器。第二個函數(若是存在的話)被看做是promise被明確拒絕時,或者任何錯誤/異常在解析的過程當中被捕捉到時要調用的處理器。
若是這兩個參數值之一被省略或者不是一個合法的函數 —— 一般你會用null來代替 —— 那麼一個佔位用的默認等價物就會被使用。默認的成功回調將傳遞它的完成值,而默認的錯誤回調將傳播它的拒絕理由。
調用then(null,handleRejection)的縮寫是catch(handleRejection)。
then(..)和catch(..)二者都自動地構建並返回另外一個promise實例,它被連接在本來的promise上,接收本來的promise的解析結果 —— (實際被調用的)完成或拒絕處理器返回的任何值。考慮以下代碼:併發
ajax( "http://some.url.1" ) .then( function fulfilled(contents){ return contents.toUpperCase(); }, function rejected(reason){ return "DEFAULT VALUE"; } ) .then( function fulfilled(data){ // 處理來自於本來的promise的處理器中的數據 } );
在這個代碼段中,咱們要麼從fulfilled(..)返回一個當即值,要麼從rejected(..)返回一個當即值,而後在下一個事件週期中這個當即值被第二個then(..)的fulfilled(..)接收。若是咱們返回一個新的promise,那麼這個新promise就會做爲解析結果被歸入與採用:app
ajax( "http://some.url.1" ) .then( function fulfilled(contents){ return ajax( "http://some.url.2?v=" + contents ); }, function rejected(reason){ return ajax( "http://backup.url.3?err=" + reason ); } ) .then( function fulfilled(contents){ // `contents` 來自於任意一個後續的 `ajax(..)` 調用 } );
要注意的是,在第一個fulfilled(..)中的一個異常(或者promise拒絕)將 不會 致使第一個rejected(..)被調用,由於這個處理僅會應答第一個原始的promise的解析。取代它的是,第二個then(..)調用所針對的第二個promise,將會收到這個拒絕。
在上面的代碼段中,咱們沒有監聽這個拒絕,這意味着它會爲了將來的觀察而被靜靜地保持下來。若是你永遠不經過調用then(..)或catch(..)來觀察它,那麼它將會成爲未處理的。有些瀏覽器的開發者控制檯可能會探測到這些未處理的拒絕並報告它們,可是這不是有可靠保證的;你應當老是觀察promise拒絕。
注意: 這只是Promise理論和行爲的簡要概覽。異步
Promise是Promise(..)構造器的純粹實例。然而,還存在稱爲 thenable 的類promise對象,它一般能夠與Promise機制協做。
任何帶有then(..)函數的對象(或函數)都被認爲是一個thenable。任何Promise機制能夠接受與採用一個純粹的promise的狀態的地方,均可以處理一個thenable。
Thenable基本上是一個通常化的標籤,標識着任何由除了Promise(..)構造器以外的其餘系統建立的類promise值。從這個角度上講,一個thenable沒有一個純粹的Promise那麼可信。例如,考慮這個行爲異常的thenable:異步編程
var th = { then: function thener( fulfilled ) { // 永遠會每100ms調用一次`fulfilled(..)` setInterval( fulfilled, 100 ); } };
若是你收到這個thenable並使用th.then(..)將它連接,你可能會驚訝地發現你的完成處理器被反覆地調用,而普通的Promise本應該僅僅被解析一次。
通常來講,若是你從某些其餘系統收到一個聲稱是promise或thenable的東西,你不該當盲目地相信它。在下一節中,咱們將會看到一個ES6 Promise的工具,它能夠幫助解決信任的問題。
可是爲了進一步理解這個問題的危險,讓咱們考慮一下,在 任何 一段代碼中的 任何 對象,只要曾經被定義爲擁有一個稱爲then(..)的方法就都潛在地會被誤認爲是一個thenable —— 固然,若是和Promise一塊兒使用的話 —— 不管這個東西是否有意與Promise風格的異步編碼有一絲關聯。
在ES6以前,對於稱爲then(..)的方法歷來沒有任何特別的保留措施,正如你能想象的那樣,在Promise出如今雷達屏幕上以前就至少有那麼幾種狀況,它已經被選擇爲方法的名稱了。最有可能用錯thenable的狀況就是使用then(..)的異步庫不是嚴格兼容Promise的 —— 在市面上有好幾種。
這份重擔將由你來肩負:防止那些將被誤認爲一個thenable的值被直接用於Promise機制。函數
PromiseAPI還爲處理Promise提供了一些靜態方法。
Promise.resolve(..)建立一個被解析爲傳入的值的promise。讓咱們將它的工做方式與更手動的方法比較一下:
var p1 = Promise.resolve( 42 ); var p2 = new Promise( function pr(resolve){ resolve( 42 ); } );
p1和p2將擁有徹底相同的行爲。使用一個promise進行解析也同樣:
var theP = ajax( .. ); var p1 = Promise.resolve( theP ); var p2 = new Promise( function pr(resolve){ resolve( theP ); } );
提示: Promise.resolve(..)就是前一節提出的thenable信任問題的解決方案。任何你還不肯定是一個可信promise的值 —— 它甚至多是一個當即值 —— 均可以經過傳入Promise.resolve(..)來進行規範化。若是這個值已是一個可識別的promise或thenable,它的狀態/解析結果將簡單地被採用,將錯誤行爲與你隔絕開。若是相反它是一個當即值,那麼它將會被「包裝」進一個純粹的promise,以此將它的行爲規範化爲異步的。
Promise.reject(..)建立一個當即被拒絕的promise,與它的Promise(..)構造器對等品同樣:
var p1 = Promise.reject( "Oops" ); var p2 = new Promise( function pr(resolve,reject){ reject( "Oops" ); } );
雖然resolve(..)和Promise.resolve(..)能夠接收一個promise並採用它的狀態/解析結果,可是reject(..)和Promise.reject(..)不會區分它們收到什麼樣的值。因此,若是你使用一個promise或thenable進行拒絕,這個promise/thenable自己將會被設置爲拒絕的理由,而不是它底層的值。
Promise.all([ .. ])接收一個或多個值(例如,當即值,promise,thenable)的數組。它返回一個promise,這個promise會在全部的值完成時完成,或者在這些值中第一個被拒絕的值出現時被當即拒絕。
使用這些值/promises:
var p1 = Promise.resolve( 42 ); var p2 = new Promise( function pr(resolve){ setTimeout( function(){ resolve( 43 ); }, 100 ); } ); var v3 = 44; var p4 = new Promise( function pr(resolve,reject){ setTimeout( function(){ reject( "Oops" ); }, 10 ); } );
讓咱們考慮一下使用這些值的組合,Promise.all([ .. ])如何工做:
Promise.all( [p1,p2,v3] ) .then( function fulfilled(vals){ console.log( vals ); // [42,43,44] } ); Promise.all( [p1,p2,v3,p4] ) .then( function fulfilled(vals){ // 永遠不會跑到這裏 }, function rejected(reason){ console.log( reason ); // Oops } );
Promise.all([ .. ])等待全部的值完成(或第一個拒絕),而Promise.race([ .. ])僅會等待第一個完成或拒絕。考慮以下代碼:
// 注意:爲了不時間的問題誤導你, // 重建全部的測試值! Promise.race( [p2,p1,v3] ) .then( function fulfilled(val){ console.log( val ); // 42 } ); Promise.race( [p2,p4] ) .then( function fulfilled(val){ // 永遠不會跑到這裏 }, function rejected(reason){ console.log( reason ); // Oops } );
警告: 雖然 Promise.all([])將會當即完成(沒有任何值),可是 Promise.race([])將會被永遠掛起。這是一個奇怪的不一致,我建議你應當永遠不要使用空數組調用這些方法。
將一系列promise在一個鏈條中表達來表明你程序的異步流程控制是 可能 的。考慮如以下代碼:
step1() .then( step2, step1Failed ) .then( function step3(msg) { return Promise.all( [ step3a( msg ), step3b( msg ), step3c( msg ) ] ) } ) .then(step4);
可是對於表達異步流程控制來講有更好的選項,並且在代碼風格上可能比長長的promise鏈更理想。咱們可使用在第三章中學到的generator來表達咱們的異步流程控制。
要識別一個重要的模式:一個generator能夠yield出一個promise,而後這個promise可使用它的完成值來推動generator。
考慮前一個代碼段,使用generator來表達:
function *main() { try { var ret = yield step1(); } catch (err) { ret = yield step1Failed( err ); } ret = yield step2( ret ); // step 3 ret = yield Promise.all( [ step3a( ret ), step3b( ret ), step3c( ret ) ] ); yield step4( ret ); }
從表面上看,這個代碼段要比前一個promise鏈等價物要更繁冗。可是它提供了更加吸引人的 —— 並且重要的是,更加容易理解和閱讀的 —— 看起來同步的代碼風格(「return」值的=賦值操做,等等),對於try..catch錯誤處理能夠跨越那些隱藏的異步邊界使用來講就更是這樣。
爲何咱們要與generator一塊兒使用Promise?不用Promise進行異步generator編碼固然是可能的。
Promise是一個可信的系統,它將普通的回調和thunk中發生的控制倒轉(參見本系列的 異步與性能)反轉回來。因此組合Promise的可信性與generator中代碼的同步性有效地解決了回調的主要缺陷。另外,像Promise.all([ .. ])這樣的工具是一個很是美好、乾淨的方式 —— 在一個generator的一個yield步驟中表達併發。
那麼這種魔法是如何工做的?咱們須要一個能夠運行咱們generator的 運行器(runner),接收一個被yield出來的promise並鏈接它,讓它要麼使用成功的完成推動generator,要麼使用拒絕的理由向generator拋出異常。
許多具有異步能力的工具/庫都有這樣的「運行器」;例如,Q.spawn(..)和個人asynquence中的runner(..)插件。這裏有一個獨立的運行器來展現這種處理如何工做:
function run(gen) { var args = [].slice.call( arguments, 1), it; it = gen.apply( this, args ); return Promise.resolve() .then( function handleNext(value){ var next = it.next( value ); return (function handleResult(next){ if (next.done) { return next.value; } else { return Promise.resolve( next.value ) .then( handleNext, function handleErr(err) { return Promise.resolve( it.throw( err ) ) .then( handleResult ); } ); } })( next ); } ); }
注意: 這個工具的更豐富註釋的版本,參見本系列的 異步與性能。另外,由各類異步庫提供的這種運行工具一般要比咱們在這裏展現的東西更強大。例如,asynquence的runner(..)能夠處理被yield的promise、序列、thunk、以及(非promise的)間接值,給你終極的靈活性。
因而如今運行早先代碼段中的*main()就像這樣容易:
run( main ) .then( function fulfilled(){ // `*main()` 成功地完成了 }, function rejected(reason){ // 噢,什麼東西搞錯了 } );
實質上,在你程序中的任何擁有多於兩個異步步驟的流程控制邏輯的地方,你就能夠 並且應當 使用一個由運行工具驅動的promise-yielding generator來以一種同步的風格表達流程控制。這樣作將產生更易於理解和維護的代碼。
這種「讓出一個promise推動generator」的模式將會如此常見和如此強大,以致於ES6以後的下一個版本的JavaScript幾乎能夠肯定將會引入一中新的函數類型,它無需運行工具就能夠自動地執行。
隨着JavaScript在它被普遍採用過程當中的日益成熟與成長,異步編程愈加地成爲關注的中心。對於這些異步任務來講回調並不徹底夠用,並且在更精巧的需求面前全面崩塌了。
可喜的是,ES6增長了Promise來解決回調的主要缺陷之一:在可預測的行爲上缺少可信性。Promise表明一個潛在異步任務的將來完成值,跨越同步和異步的邊界將行爲進行了規範化。
可是,Promise與generator的組合才徹底揭示了這樣作的好處:將咱們的異步流程控制代碼從新安排,將難看的回調漿糊(也叫「地獄」)弱化並抽象出去。
目前,咱們能夠在各類異步庫的運行器的幫助下管理這些交互,可是JavaScript最終將會使用一種專門的獨立語法來支持這種交互模式!