Promise: 給我一個承諾,我還你一個承諾

處理concurrent programming,除了threading/multi-processing外,各家語言都有自己的絕活:erlang/elixir是actor model,golang/clojure(core.async)是CSP,haskell/clojure是STM,而javascript是event loop/callback。

callback可能是這幾種並發模型裡面最好懂的一種方式,就像好萊塢經紀人慣常的做法:don』t call me, I』ll call you back。比如打開資料庫,打開要訪問的表,寫入一列新的數據這樣一系列IO密集型的操作,如果同步去做,等待的時間要遠大於運算的時間,而使用callback非同步處理則消除了等待,大大增強了軟體的並行性。然而,callback理解起來很直觀,寫起代碼來很費勁,稍微複雜一些的處理,很容易搞成下圖這樣的pyramid of doom,也就是俗稱的callback hell:

當然,你可以通過重構,把嚴重嵌套,影響閱讀的pyramid拆分成若干個小的pyramid,減少眼睛出血(eye-bleeding)的概率,但畢竟治標不治本。於是,在各種版本的第三方javascript類庫里,大家都實現了各自的Promise/A+對象,來減少對callback的依賴。

Promise是這樣一個對象,對於任意的非同步操作,它提供了一組固定的API,才操作這個結果。我們先看一段代碼:

我們看到,如果要把一個非同步操作封裝成Promise,我們需要首先創建一個Promise對象,並提供一個包含兩個參數 resolve,reject的函數,在這個函數里調用你的非同步方法(這裡用setTimeout模擬)。如果非同步方法成功,則在其callback裡面調用 resolve,提供成功後獲得的數據;如果失敗,則調用 reject,提供錯誤數據。這一般是類庫提供者(producer)要做的事情。

對於類庫調用者(consumer),拿到一個Promise對象,他可以調用 then 方法來獲取非同步後的數據,也可以調用 catch 來處理錯誤。Promise提供了如下機制來簡化consumer的代碼:

  • then 依舊返回一個Promise,這樣,代碼的撰寫由視覺上的橫向延伸(callback hell)變成縱向擴展(chained operation),可讀性增強

  • error propagation,在若干個Promise間不斷chain的過程中,期間發生的任何error都會被一路傳遞到最後的Promise的 reject,方便程序員用一個 catch 捕獲一條鏈上的錯誤,同樣的,可讀性大大增強

我們看之前那個callback-hell使用Promise撰寫後的代碼:

代碼清晰了不少。下面是Promise處理的狀態機:

在ES5,Promise並非原生支持,但有很多第三方的類庫支持;在ES6中,Promise形成了一個標準,並且在語言層面原生支持。

Promise在實際使用中除了解決callback hell,讓代碼可讀性增強外,還可以做很多事情。因為Promise實際上可以被視作一個Monad,所以你可以將其用在很多本來難以做composition的場合。比如你有一個處理,需要依賴多個數據源,他們或同步(數據已經在內存中直接可讀),或非同步(數據需要從資料庫或者文件系統讀取,甚至來自第三方API),正常來說似乎很難被抽象成一個數據結構。然而,你可以將這些數據源統統封裝成Promise(同步的數據可以被視作一個狀態立即走到resolved的Promise),這樣,可以統一處理。比如說 Promise.all(iterable)(resolve所有結果,返回新的Promise),或者 Promise.race(iterable)(只要有一個結果resolve出來,就立即返回新的Promise,典型的anycast使用場景)。

目前nodejs的庫函數還是callback方式,雖說手工轉換成Promise非常簡單,但畢竟不那麼方便。在nodejs app里,你可以使用bluebird(或者es6-promisify)來批量轉化nodejs的標準庫。比如:

轉化之後可以這麼使用:

最後,說說Promise的缺點。

第一,一個Promise只能resolve單個數據,對應於同步處理里的單值數據;如果要處理非同步場景下的 "array",那麼,Observable是更好的方式。

第二,Promise的API設計有些缺陷,並非lazy(可能是歷史原因,也可能考慮到API友好程度),一旦啟動,不可終止。如果你需要處理可終止的非同步操作,那麼,也需要使用Observable。下面是Promise和Observable的代碼的對比,可以看到,一旦創建,Promise會立刻執行對象體內的代碼(不管你有沒有調用 then),而Observable直到subscriber真正要讀取時(forEach)才會求值,而且,Observable提供了cancel的API:

即便Oberservable已經開始運行,只要還未完成,調用者都有機會終止它。

OK,今天就先講到這裡,以後我們再講Observable。

延伸閱讀:MDN上的Promise介紹。

如果您覺得這篇文章不錯,請點贊。多謝!

歡迎訂閱公眾號『程序人生』(搜索微信號 programmer_life)。每篇文章都力求原汁原味,北京時間中午12點左右,美西時間下午8點左右與您相會。


推薦閱讀:

閑扯設備廠商的轉型
軟體開發升級打怪之路
為什麼你要懂點信息安全
用心與否,一試便知

TAG:迷思 | MDN上的Promise介绍 |