理解與使用Promise完成複雜非同步處理流程
本文談到的Promise是指javascript環境下的Promise,然而Promise這個功能在若干語言中均有實現,我本次會在Nodejs服務端環境下進行學習和理解。
Promise是為了解決日趨複雜的非同步程式設計而出現的,簡單的非同步例如:發起一個ajax請求來獲取資料,之後渲染DOM。
然而現實世界並沒有這麼簡單,我們極有可能需要同時發起多個ajax請求並等待它們全部返回,在獲得結果後可能又需要根據上一輪的資料發起下一輪的ajax獲取其他資料,這樣的流程完全可以演變得交織錯雜,程式設計和維護起來是非常頭疼的。
此前,我們解決這種問題就是callback的回撥思路,callback巢狀callback的程式碼層出不窮,想追加點功能需要一層一層的數括號,這就急需一個替代方案的出現。
Promise應運而生!
Promise提供了序列/並行非同步程式設計的簡化方案,ajax-2依賴ajax-1,或者ajax3依賴ajax-1+ajax2,都可以輕鬆通過它輕鬆實現。
學習Promise的文章挺多的,我個人感覺沒有說的特別明白的部落格。但是,我建議你先看看這個,然後跟著我下面的例子來理解一下就行。
談談我對Promise的理解
Promise物件有一個狀態,表示非同步處理的一個進度,可以是:pending(等待結果),resolved(已完成),rejected(已失敗);另外還有一個儲存的區域,它用於放置非同步處理完成後(resolved or rejected)的結果資料,可以是任意格式。
既然Promise本身只有這個狀態而已,那麼顯然需要我們業務程式碼去驅動它的進度變遷,否則它就是一坨不會動的程式碼。這裡,Promise的建構函式需要傳入1個接受2個引數的function,我們一般是這樣用的:
new Promise( /* executor */ function(resolve, reject) { ... } );
這個函式提供了2個function給業務呼叫,呼叫Resolve就可以改變這個Promise的狀態為resolved,同樣道理呼叫reject就可以讓Promise的狀態變為rejected。
resolve()/reject()函式接受1個入參,它會被傳遞給後面串聯的.then()呼叫,這個入參可以是一個普通的物件,也可以是一個Promise物件。重要的是,如果是一個Promise物件,那麼這個後面串聯的then()需要等到這個Promise的狀態等於終結狀態(非pending)後才會被回撥,而then()回撥的傳入值是也就是這個Promise最終resolve/reject傳入的值。
有點繞吧,可以先忘記這一個段落,看看then()做了什麼再回頭理解。
那麼,then()又是做什麼的呢?
其實也很簡單,如果你在上述業務邏輯裡呼叫了resolve,那麼Promise非同步處理相當於終結了,then()就是指前一個Promise終結後再做什麼事情的意思。
then()的函式原型也不復雜,要求我們傳入一個function,當Promise通過resolve(xxx)/reject(xxx)終結後,xxx會被當做入參呼叫這個function,我們可以在裡面做下一步的事情,從而實現串聯的感覺。
值得注意的是,Promise為了實現.then()呼叫的串聯(只有Promise物件有then方法),.then()的回撥函式的返回值會被隱式的轉換為Promise物件(如果你沒有顯式的返回Promise物件),這是then實現內部通過Promise.resolve/Promise.reject這兩個API實現的,我會在後面的例子中體現這個事情。
實踐出真知
為了加深理解,我親手寫了8個demo來看體驗promise的各種特性,我這裡逐一列出來做一個簡短的說明。
如果實在理解有困難可以把程式碼拉到本地,使用node promise.js來執行除錯一下。
// 同步resolve var promise1 = new Promise( (resolve, reject) => { resolve("this is promise1 resolve"); } ).then( (msg) => { console.log(msg); }, (err) => { console.log(err); } );
var promise = 這部分可以無視,我僅僅用於程式碼裡標記一下demo的次序。這個例子體現了最基礎用法,給resolve傳入一個字串終結當前的Promise的狀態,因為Promise被終結,因此該字串會被回撥給then中的(msg) => {...}函式,從而實現串聯。
// 同步reject var promise2 = new Promise( (resolve, reject) => { reject("this is promise2 reject"); } ).then( (msg) => { console.log(msg); }, (err) => { console.log(err); } );
和上個例子差不多,只是呼叫了reject,這樣會回撥(err) => {....}。
// 同步catch var promise3 = new Promise( (resolve, reject) => { reject("this is promise3 reject catch"); } ).then( (msg) => { console.log(msg); } ).catch( (err) => { console.log(err); } );
如果我沒有在then()裡提供reject的回撥函式,那麼這個reject事件會繼續向後移動,直到遇到catch會被處理。
// 非同步resolve var promise4 = new Promise( (resolve, reject) => { var promise4_1 = new Promise( (resolve, reject) => { console.log("promise4_1 starts"); setTimeout( () => { resolve("this is promise4_1 resolve"); }, 2000 ); } ); resolve(promise4_1); } ).then( (msg) => { console.log(msg); }, (err) => { console.log(err); } );
這裡,我故意營造了一個resolve(Promise Object)的例子(也就是promise4_1),這樣的話then()會等到這個Promise Object自身的非同步流程處理結束後再回調,這相當於為promise4非同步流程節外生枝了promise4_1,等枝葉長成後再回到promise4主幹繼續向後鏈式處理。
// 鏈式resolve var promise5 = new Promise( (resolve, reject) => { var promise4_1 = new Promise( (resolve, reject) => { console.log("promise5_1 starts"); setTimeout( () => { resolve("this is promise5_1 resolve"); }, 2000 ); } ); resolve(promise4_1); } ).then( (msg) => { console.log(msg); var promise5_2 = new Promise( (resolve, reject) => { console.log("promise5_2 starts"); setTimeout( () => { resolve("this is promise5_2 resolve"); }, 2000 ); } ); return promise5_2; } ).then( (msg) => { console.log(msg); throw new Error(); } ).catch( () => { console.log("exception catched after promise5_2 resolved"); } );
這個例子變得再複雜一些,除了在promise5中節外生枝promise5_1非同步處理2秒,在2秒後回到主幹後的.then()環節,我通過return返回一個Promise物件再次節外生枝promise5_2非同步執行2秒,之後再次回到主幹的.then()打印出訊息並且丟擲了異常,最終由catch捕獲。
// 並行+鏈式promise var promise6 = new Promise( (resolve, reject) => { var promiseArr = []; for (var i = 0; i < 5; ++i) { promiseArr.push(new Promise( (resolve, reject) => { console.log(`promise6_${i} starts`); ((index) => { // 閉包處理i setTimeout( () => { console.log(`before promise6_${index} resolved`); resolve(`this is promise6_${index} resolve`); }, index * 1000 ); })(i); } )); } resolve(Promise.all(promiseArr)); } ).then( (msgArr) => { console.log(`promise6 all resolved ${msgArr}`); } );
這個例子主要是體驗Promise.all(),這個函式其實建立返回了一個Promise物件,內部管理與併發了多個Promise流程(節外生枝了N個樹叉),它等待它們全部完成或者任意失敗之後會終結自己,在外層通過resolve將Promise.all()返回的集合式Promise物件串聯(託管)起來,最終進入下一個then從而可以訪問N個樹叉的結果集合。
// .then()隱式包裝resolved Promise var promise7 = new Promise( (resolve, reject) => { var promise7_1 = new Promise( (resolve, reject) => { console.log("promise7_1 starts"); setTimeout( () => { resolve("this is promise7_1 resolve"); }, 2000 ); } ); resolve(promise7_1); } ).then( (msg) => { console.log(msg); return "promise7 .then()隱式包裝resolved Promise"; }, (err) => { console.log(err); } ).then( (word) => { console.log(word); } );
這個例子除了節外生枝外,主要關注在於第1個.then()中return了一個字串,它實際被隱式的包裝成了一個resolved狀態的Promise物件返回(這是我想強調的重點),從而繼續鏈式的呼叫第2個.then()的(word) => {...}回撥函式。
// .then()顯式包裝resolved Promise var promise8 = new Promise( (resolve, reject) => { var promise8_1 = new Promise( (resolve, reject) => { console.log("promise8_1 starts"); setTimeout( () => { resolve("this is promise8_1 resolve"); }, 2000 ); } ); resolve(promise8_1); } ).then( (msg) => { console.log(msg); return Promise.resolve("promise8 .then()顯式包裝resolved Promise"); }, (err) => { console.log(err); } ).then( (word) => { console.log(word); } );
這個例子和上一個例子等價,這裡體現了第1個.then()顯式呼叫Promise.resolve返回一個Promise物件,從而第2個.then()回撥(word) => {}。
// .then()顯式包裝rejected Promise var promise9 = new Promise( (resolve, reject) => { var promise9_1 = new Promise( (resolve, reject) => { console.log("promise9_1 starts"); setTimeout( () => { resolve("this is promise9_1 resolve"); }, 2000 ); } ); resolve(promise9_1); } ).then( (msg) => { console.log(msg); return Promise.reject("promise9 .then()顯式包裝rejected Promise"); }, (err) => { console.log(err); } ).catch( (word) => { console.log(word); } );
這個例子和上面2個例子相反,我在第1個.then()顯式的返回了一個rejected的Promise物件,這是通過Promise.reject包裝字串而成的,因此catch將被呼叫。
通過最後3個例子,我們應該可以明確的感受到Promise圍繞pending,resolved,rejected三個狀態實現的非同步狀態驅動以及串聯/並行呼叫的觸發動機與原理。
關於Promise本身的功能就瞭解這麼多,希望後面有機會在react下多多使用,解決一些併發ajax以及串聯ajax的非同步需求,關鍵還是找到應用場景進行合理的套用,這是我認為最難的地方。
另外,需要記住Promise是ES6的產物,而未來ES7提出了async/await關鍵字將對Promise加以利用進一步簡化非同步程式設計,它將更接近於協程的理念,更加符合人類的思考習慣,至少我是這麼認為的。