在ECMAScript 6中內建Promise,可將非同步的程式流程,從回呼的抽象層次提升到Promise,令開發者撰寫非同步流程時,就像撰寫同步流程一樣直覺。在Python中,直接跳過Promise抽象,在3.4引入asyncio,在3.5引入async、await語法。如果你曾接觸這兩者,是否想過它們的差異性?

ECMAScript 6的Promise

在Node.js或者是瀏覽器中,JavaScript的運行流程基本上是同步方式,然而,有些時候會是非同步,例如使用XMLHttpRequest請求資源時,在呼叫send方法之後,程式流程會繼續,在請求狀態變化時,瀏覽器呼叫對應的處理器進行處理,然而,若希望特定狀態發生時,依序執行下一次非同步流程,就容易引發回呼地獄的問題。

Promise是ECMAScript 6新增的標準API,在建立Promise實例時,狀態處於未定(PENDING),建立Promise時,可傳入一個回呼函式,該函式具有兩個參數,可命名為resolve與reject,這兩個參數會各自接受函式,若呼叫resolve,Promise會處於達成(FULFILLED)狀態,若呼叫reject,Promise會是被背棄(REJECTED)的狀態。

若Promise實例曾使用then組合下一次非同步操作,在處於達成狀態時,會呼叫then指定的第一個函式,若處於背棄狀態,會呼叫then指定的第二個函式,從而可針對這兩個狀態分別設定對應的處理,then也會傳回一個Promise,因此,流程上可循序進行對應的處理組合,雖說本質上在處理時是非同步,然而撰寫風格上,就會像是同步的。Promise的then也可以接受Promise,使用的情況是不在乎前一個Promise的結果,只需要在前一個Promise完成後執行時使用。

ECMAScript還支援產生器語法,當Promise與產生器結合時,可以產生有趣的操作風格,相較於使用asyncFoo(10).then(r1 => asyncFoo(r1)).then(r2 => asyncFoo(r2)).then(r3 => console.log(r3)),底下方式更像是內建語法的一部份:

async(function*() {
let r1 = yield asyncFoo(10);
let r2 = yield asyncFoo(r1);
let r3 = yield asyncFoo(r2);
console.log(r3);
});

實現一個Promise

在ECMAScript 6中,上述的async是個可基於產生器自行實現的函式,可參考〈Promise〉(https://goo.gl/MSmpLG)的說明,實際上,在ECMAScript 7新增了async、await,在語法層面提供了這類支援,不用自行撰寫一個async函式了,而await在語意上,也會比yield來得清楚:

async function task() {
let r1 = await asyncFoo(10);
let r2 = await asyncFoo(r1);
let r3 = await asyncFoo(r2);
console.log(r3);
}

大部份介紹Promise、async、await的文件,都是這麼一路談過來,現在反過來吧!你已經知道怎麼實現之前的async函式,是否想過怎麼自行撰寫個Promise嗎?

本質上,Promose就是個狀態機,會有未定、達成與背棄的狀態,無論是resolve或reject,傳給它們的值都會被Promise保留,Promise也必須知道達成或背棄時,需分別呼叫的處理器,因而一個最簡單的Promise,內部只要保留狀態、值與處理器三個值。

在註冊處理器時,如果Promise尚處於未定狀態,處理器會先被放入容器之中,若已處於達成或背棄狀態,就直接將結果(背棄狀態時通常是個代表錯誤的值,像是例外實例)作為引數呼叫處理器;如果Promise從未定狀態轉移至達成或背棄狀態,會逐一取得被放入容器中的處理器來執行,由於是單一執行緒,未定或達成、背棄狀態時加入的處理器,一定都會被執行。

Promise的then會傳回Promise,因此呼叫then時,前一個Promise實例會被封裝在新建的Promise之中,前一個Promise離開未定狀態之後,取得的結果會用來呼叫新建Promise時,傳入的resolve或reject函式。如果想看看以上Promise概念的簡單實現,可以參考〈Promise Implementing〉(https://goo.gl/qbcmKa)的內容。

實現一個事件迴圈

在ECMAScript 7中有async、await,一個async函式會自動傳回Promise,一個await可以等待Promise,使用async、await定義函式之後,直接呼叫該函式,就直接擁有非同步的好處,然而,如果你看過Python 3.5的async、await,可能會納悶,在Python中,為什麼還要取得一個事件迴圈,才能進一步執行非同步函式?

因為,在Python中定義一個async函式並執行之後,實際會傳回一個coroutine實例,這個實例與generator實例有點類似,有send與throw方法,實際上,在3.4時提出的asyncio有個@asyncio.coroutine,可將產生器函式轉換為coroutine函式,在Python 3.5中,如果沒有包袱的話,建議使用@types.coroutine取代,在async函式中,只有coroutine(或者實現了__await__的)實例才可以搭配await來使用。

執行一個async函式只會傳回coroutine實例,須呼叫coroutine實例的send方法(或者throw引發例外),才會執行函式本體,因此,必須有個類似先前Promise提到的async函式,當await將流程控制權交還給coroutine實例send方法的呼叫端時,由呼叫端再次呼叫send方法,這就形成了事件迴圈的概念。

在〈A tale of event loops〉(https://goo.gl/4CQuq6),就藉由自行實現事件迴圈,以便進一步理解,看看Python的asyncio.get_event_loop().run_until_complete()這個方法,在底層究竟做了哪些事。想深入認識Python的asyncio程式庫,以及async與await語法,認識事件迴圈是必要的一環。

非同步的抽象層次

就非同步的抽象層次來說,最低階的部份是狀態機的概念,JavaScript的回呼,像是Ajax或Node.js的輸入輸出處理,則是第二個抽象層次,而JavaScript界目前常見的Promise(早在ECMAScript 6之前就有Promise的標準與實現),則是非同步處理的第三個抽象層次。

ECMAScript 7中的async、await是第四個層次,JavaScript本身是執行在一個主事件迴圈中,而呼叫一個async函式時,它會立即在主事件迴圈中執行,某些程度上,這與JavaScript的天性相符合,對開發者在撰寫程式時也比較直覺,然而缺點是無法在JavaScript中,直接基於async、await來實現自己的事件迴圈程式庫。

在Python中,直接跳過了Promise抽象(雖然在PyPi上也有promise模組的實現)而直接採用了asyncio以及async、await,願意的話,就算不依賴asyncio程式庫,自行實現一個事件迴圈程式庫也是可行的,就語義上,明確地取得事件迴圈並安排非同步處理函式,也符合Python的精神。

儘管越來越多語言,都開始支援async、await語法,瞭解不同語言在非同步的支援,仍是很有趣的一件事,狀態機到async、await的抽象層次,也只是個大致的分類,實際上還有其他的實現方式,像是Future、Selector等,都是值得探討的概念,有機會試著自行實現相關概念,也會是實用的體驗。

作者簡介


Advertisement

更多 iThome相關內容