天天看點

講講 Promise

<code>Promise</code> 最早出現在 1988 年,由 Barbara Liskov、Liuba Shrira 首創(論文:Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems)。并且在語言 MultiLisp 和 Concurrent Prolog 中已經有了類似的實作。

JavaScript 中,<code>Promise</code> 的流行是得益于 jQuery 的方法 <code>jQuery.Deferred()</code>,其他也有一些更精簡獨立的 <code>Promise</code> 庫,例如:Q、When、Bluebird。

由于 jQuery 并沒有嚴格按照規範來制定接口,促使了官方對 <code>Promise</code> 的實作标準進行了一系列重要的澄清,該實作規範被命名為 Promise/A+。後來 ES6(也叫 ES2015,2015 年 6 月正式釋出)也在 Promise/A+ 的标準上官方實作了一個 <code>Promise</code> 接口。

想要實作一個 <code>Promise</code>,必須要遵循如下規則:

<code>Promise</code> 是一個提供符合标準的 <code>then()</code> 方法的對象。

初始狀态是 <code>pending</code>,能夠轉換成 <code>fulfilled</code> 或 <code>rejected</code> 狀态。

一旦 <code>fulfilled</code> 或 <code>rejected</code> 狀态确定,再也不能轉換成其他狀态。

一旦狀态确定,必須要傳回一個值,并且這個值是不可修改的。

ECMAScript's Promise global is just one of many Promises/A+ implementations.

主流語言對于 <code>Promise</code> 的實作:Golang/go-promise、Python/promise、C#/Real-Serious-Games/c-sharp-promise、PHP/Guzzle Promises、Java/IOU、Objective-C/PromiseKit、Swift/FutureLib、Perl/stevan/promises-perl。

由于 JavaScript 是單線程事件驅動的程式設計語言,通過回調函數管理多個任務。在快速疊代的開發中,因為回調函數的濫用,很容易産生被人所诟病的回調地獄問題。<code>Promise</code> 的異步程式設計解決方案比回調函數更加合理,可讀性更強。

傳說中比較誇張的回調:

現實業務中依賴關系比較強的回調:

使用 <code>Promise</code> 梳理流程後:

<code>Promise</code> 的運轉實際上是一個觀察者模式,<code>then()</code> 中的匿名函數充當觀察者,<code>Promise</code> 執行個體充當被觀察者。

在 <code>Promise</code> 出現之前往往使用回調函數管理一些異步程式的狀态。

<code>Promise</code> 出現後使用 <code>then()</code> 接收事件的狀态,且隻會接收一次。

案例:插件初始化。

使用回調函數:

使用 Promise:

相對于使用回調函數,邏輯更清晰,什麼時候初始化完成和觸發回調一目了然,不再需要重複判斷狀态和回調函數。當然更好的做法是隻給第三方輸出狀态和資料,至于如何使用由第三方決定。

<code>then()</code> 必然傳回一個 <code>Promise</code> 對象,<code>Promise</code> 對象又擁有一個 <code>then()</code> 方法,這正是 <code>Promise</code> 能夠鍊式調用的原因。

由于傳回一個 <code>Promise</code> 結構體永遠傳回的是鍊式調用的最後一個 <code>then()</code>,是以在處理封裝好的 <code>Promise</code> 接口時沒必要在外面再包一層 <code>Promise</code>。

<code>Promise.all()</code> / <code>Promise.race()</code> 可以将多個 Promise 執行個體包裝成一個 Promise 執行個體,在處理并行的、沒有依賴關系的請求時,能夠節約大量的時間。

<code>async</code> / <code>await</code> 實際上隻是建立在 <code>Promise</code> 之上的文法糖,讓異步代碼看上去更像同步代碼,是以 <code>async</code> / <code>await</code> 在 JavaScript 線程中是非阻塞的,但在目前函數作用域内具備阻塞性質。

使用 <code>async</code> / <code>await</code> 的優勢:

簡潔幹淨

寫更少的代碼,不需要特地建立一個匿名函數,放入 <code>then()</code> 方法中等待一個響應。

條件語句

當一個異步傳回值是另一段邏輯的判斷條件,鍊式調用将随着層級的疊加變得更加複雜,讓人很容易在代碼中迷失自我。使用 <code>async</code> / <code>await</code> 将使代碼可讀性變得更好。

中間值

異步函數常常存在一些異步傳回值,作用僅限于成為下一段邏輯的入場券,如果經曆層層鍊式調用,很容易成為另一種形式的“回調地獄”。

靠譜的 <code>await</code>

<code>await 'qtt'</code> 等于 <code>await Promise.resolve('qtt')</code>,<code>await</code> 會把任何不是 <code>Promise</code> 的值包裝成 <code>Promise</code>,看起來貌似沒有什麼用,但是在處理第三方接口的時候可以 “Hold” 住同步和異步傳回值,否則對一個非 <code>Promise</code> 傳回值使用 <code>then()</code> 鍊式調用則會報錯。

使用 <code>async</code> / <code>await</code> 的缺點:

<code>async</code> 永遠傳回 <code>Promise</code> 對象,不夠靈活,很多時候我隻想單純傳回一個基本類型值。

<code>await</code> 阻塞 <code>async</code> 函數中的代碼執行,在上下文關聯性不強的代碼中略顯累贅。

鍊式調用中盡量結尾跟 <code>catch</code> 捕獲錯誤,而不是第二個匿名函數。因為标準裡注明了若 <code>then()</code> 方法裡面的參數不是函數則什麼都不錯,是以 <code>catch(rejectionFn)</code> 其實就是 <code>then(null, rejectionFn)</code> 的别名。

在以上代碼中,<code>anAsyncFn()</code> 抛出來的錯誤 <code>rejectError</code> 會正常接住,但是 <code>resolveSuccess</code> 抛出來的錯誤将無法捕獲,是以更好的做法是永遠使用 <code>catch</code>。

倘若講究一點,也可以通過 <code>resolveSuccess</code> 來捕獲 <code>anAsyncFn()</code> 的錯誤,<code>catch</code> 捕獲 <code>resolveSuccess</code> 的錯誤。

通過全局屬性監聽未被處理的 Promise 錯誤。

浏覽器環境(<code>window</code>)的拒絕狀态監聽事件:

<code>unhandledrejection</code> 當 Promise 被拒絕,并且沒有提供拒絕處理程式時,觸發該事件。

<code>rejectionhandled</code> 當 Promise 被拒絕時,若拒絕處理程式被調用,觸發該事件。

注意:<code>Promise.reject()</code> 和 <code>new Promise((resolve, reject) =&gt; reject())</code> 這種方式不能直接觸發 <code>unhandledrejection</code> 事件,必須是滿足已經進行了 <code>then()</code> 鍊式調用的 <code>Promise</code> 對象才行。

當執行一個超級久的異步請求時,若超過了能夠忍受的最大時長,往往需要取消此次請求,但是 <code>Promise</code> 并沒有類似于 <code>cancel()</code> 的取消方法,想結束一個 <code>Promise</code> 隻能通過 <code>resolve</code> 或 <code>reject</code> 來改變其狀态,社群已經有了滿足此需求的開源庫 Speculation。

或者利用 <code>Promise.race()</code> 的機制來同時注入一個會逾時的異步函數,但是 <code>Promise.race()</code> 結束後主程式其實還在 <code>pending</code> 中,占用的資源并沒有釋放。

若想按順序執行一堆異步程式,可使用 <code>reduce</code>。每次周遊傳回一個 <code>Promise</code> 對象,在下一輪 <code>await</code> 住進而依次執行。

每當要使用異步代碼時,請考慮使用 <code>Promise</code>。

<code>Promise</code> 中所有方法的傳回類型都是 <code>Promise</code>。

<code>Promise</code> 中的狀态改變是一次性的,建議在 <code>reject()</code> 方法中傳遞 <code>Error</code> 對象。

確定為所有的 <code>Promise</code> 添加 <code>then()</code> 和 <code>catch()</code> 方法。

使用 <code>Promise.all()</code> 行運作多個 <code>Promise</code>。

倘若想在 <code>then()</code> 或 <code>catch()</code> 後都做點什麼,可使用 <code>finally()</code>。

可以将多個 <code>then()</code> 挂載在同一個 <code>Promise</code> 上。

<code>async</code> (異步)函數傳回一個 <code>Promise</code>,所有傳回 <code>Promise</code> 的函數也可以被視作一個異步函數。

<code>await</code> 用于調用異步函數,直到其狀态改變(<code>fulfilled</code> or <code>rejected</code>)。

使用 <code>async</code> / <code>await</code> 時要考慮上下文的依賴性,避免造成不必要的阻塞。

更多文章通路我的部落格

GitHub:mazeyqian

Blog:blog.mazey.net

繼續閱讀