本節書摘來自華章出版社《angularjs深度剖析與最佳實踐》一書中的第2章,第2.10節,作者 雪狼 破狼 彭洪偉,更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視
1.生活中的一個例子
promise解決的是異步程式設計的問題,對于生活在同步程式設計世界中的程式員來說,它可能比較難于了解,這也構成了angular入門門檻之一,本節将用生活中的一個例子對此做一個形象的講解。
假設有一個家具廠,而它有一個vip客戶張先生。
有一天張先生需要一個豪華衣櫃,于是,他打電話給家具廠說:“我需要一個衣櫃,回頭做好了給我送來”,這個操作就叫$q.defer(),也就是延期。因為這個衣櫃不是現在要的,是以張先生這是在發起一個可延期的請求。
家具廠接下了這個訂單,給他留下了一個回執号,并對他說:“我們做好了會給您送過去,放心吧”。這叫作promise,也就是給了張先生一個“承諾”。
這樣,這個defer算是正式建立了,于是他把這件事記錄在自己的日記上,并且同時記錄了回執号,這個變量叫作deferred,也就是已延期事件。
現在,張先生就不用再去想着這件事了,該做什麼做什麼,這就是“異步”請求的含義。
假設家具廠在一周後做完了這個衣櫃,并如約送到了張先生家(包郵哦,親),這就叫作deferred.resolve(衣櫃),也就是“問題已解決,這是您的衣櫃”。而這時候張先生隻要取出一下這個“衣櫃”參數就行了。而且,這個“郵包”中也不一定隻有衣櫃,還可以包含别的東西,比如廠家宣傳資料、産品名錄等。整個過程中輕松愉快,誰也沒等誰,沒有浪費任何時間。
假設家具廠在評估後發現這個規格的衣櫃我們做不了,那麼它就需要deferred.reject(理由),也就是“我們不得不拒絕您的請求,因為……”。拒絕沒有時間限制,可以發生在給出承諾之後的任何時候,甚至可能發生在快做完的時候。而且拒絕時候的參數也不僅僅限于理由,還可以包含一個道歉信,違約金之類的。總之,你想給他什麼就給他什麼,如果你覺得不會惹惱客戶,那麼不給也沒關系。
假設家具廠發現,自己正好有一個符合張先生要求的存貨,它就可以用$q.when(現有衣櫃)來兌現給張先生的承諾。于是,這件事立刻解決了,皆大歡喜。張先生可不在乎你是從頭做的還是現有的成品,隻要達到自己的品質要求就滿意了。
假設這個家具廠對客戶格外的細心,它還可以通過deferred.notify(進展情況)給張先生發送進展情況的“通知”。
這樣,整個異步流程圓滿完成!無論成功還是失敗,張先生都沒有往裡面投入任何額外的時間成本。
好,我們再擴充一下這個故事:
張先生又來訂貨了,這次他分多次訂了一張桌子,三把椅子,一張席夢思。但他不希望今天收到個桌子,明天收到個椅子,後天又得簽收一次席夢思,而是希望家具廠做好了之後一次性送過來,但是他當初又是分别下單的,那麼他就可以重新跟家具廠要一個包含上述三個承諾的新承諾,這就是$q.all([桌子承諾,椅子承諾,席夢思承諾]),這樣,他就不用再關注以前的三個承諾了,直接等待這個新的承諾完成,到時候隻要一次性簽收了前面的這些承諾就行了。
2.回調地獄和promise
通過上面這個生活中例子,相信作為讀者的你已經了解到了異步和promise的方式。為什麼我們需要promise呢?
javascript是一門很靈活的語言,由于它寄宿在浏覽器中以事件機制為核心,是以在我們的javascript編碼中存在很多的回調函數。這是一個高性能的程式設計模式,是以它衍生出了基于異步i/o的高性能nodejs平台。但是如果不注意我們的編碼方法,那麼我們就會陷入“回調地獄”,也有人稱為“回調金字塔”。嵌套式的回調地獄,代碼将會變得像意大利面條一樣。如下邊的嵌套回調函數一樣:
這樣嵌套的回調函數,讓我們的代碼的可讀性變得很差,而且很難于調試和維護。是以為了降低異步程式設計的複雜性,開發人員一直尋找簡便的方法來處理異步操作。其中一種處理模式稱為promise,它代表了一種可能會長時間運作而且不一定必須完成的操作的結果。這種模式不會阻塞和等待長時間的操作完成,而是傳回一個代表了承諾的(promised)結果的對象。它通常會實作一種名叫then的方法,用來注冊狀态變化時對應的回調函數。
promise在任何時刻都處于以下三種狀态之一:未完成(pending)、已完成(resolved)和拒絕(rejected)三個狀态。以commonjs promise/a 标準為例,promise對象上的then方法負責添加針對已完成和拒絕狀态下的處理函數。then方法會傳回另一個promise對象,以便于形成promise管道,這種傳回promise對象的方式能夠讓開發人員把異步操作串聯起來,如then(resolvedhandler, rejectedhandler)。resolvedhandler回調函數在promise對象進入完成狀态時會觸發,并傳遞結果;rejectedhandler函數會在拒絕狀态下調用。
是以我們上邊的嵌套回調函數可以修改為:
async1().then(async2).then(async3).catch(showerror);
這下代碼看着清爽多了,我們不再需要忍受嵌套的無底深淵。
在es6的标準版中已經包含了promise的标準,很快它就将會從浏覽器本身得到更好的支援。與此同時在es6的标準版中,還引入了python這類語言中的generator(疊代器的生成器)概念,它本意并不是為異步而生的,但是它擁有天然的yield暫停函數執行的能力,并儲存上下文,再次調用時恢複當時的狀态,是以它也被很好地運用于javascript的異步程式設計模型中,其中最出名的案例當屬node express的下一代架構koa了。
最後還有個好消息,在es7的标準中将有可能引入async和await這兩個關鍵詞,來更大的簡化我們的javascript異步程式設計模型。我們就可以如下的方式以同步的方式編寫我們的異步代碼:
angular中的promise
在angular中大量使用着promise,最簡單的是$timeout的實作,我拷貝過來并加上了注釋: