用 Async 函式簡化非同步程式碼

發表於2017-04-08

Promise 在 JavaScript 上釋出之初就在網際網路上流行了起來 — 它們幫開發人員擺脫了回撥地獄,解決了在很多地方困擾 JavaScript 開發者的非同步問題。但 Promises 也遠非完美。它們一直請求回撥,在一些複雜的問題上仍會有些雜亂和一些難以置信的冗餘。

隨著 ES6 的到來(現在被稱作 ES2015),除了引入 Promise 的規範,不需要請求那些數不盡的庫之外,我們還有了生成器。生成器可在函式內部停止執行,這意味著可把它們封裝在一個多用途的函式中,我們可在程式碼移動到下一行之前等待非同步操作完成。突然你的非同步程式碼可能就開始看起來同步了。

這只是第一步。非同步函式因今年加入 ES2017,已進行標準化,本地支援也進一步優化。非同步函式的理念是使用生成器進行非同步程式設計,並給出他們自己的語義和語法。因此,你無須使用庫來獲取封裝的實用函式,因為這些都會在後臺處理。

執行文章中的 async/await 例項,你需要一個能相容的瀏覽器。

執行相容

在客戶端,Chrome、Firefox 和 Opera 能很好地支援非同步函式。

用 Async 函式簡化非同步程式碼

(點選圖片進行頁面跳轉)

從 7.6 版本開始,Node.js 預設啟用 async/await。

非同步函式和生成器對比

這有個使用生成器進行非同步程式設計的例項,用的是 Q 庫:

Q.async 是個封裝函式,處理場景後的事情。其中 * 表示作為一個生成器函式的功能,yield 表示停止函式,並用封裝函式代替。Q.async 將會返回一個函式,你可對它賦值,就像賦值 doAsyncOp 一樣,隨後再呼叫。

ES7 中的新語法更簡潔,操作示例如下:

差異不大,我們刪除了一個封裝的函式和 * 符號,轉而用 async 關鍵字代替。yield 關鍵字也被 await 取代。這兩個例子事實上做的事是相同的:在 asynchronousOperation 完成之後,賦值給 val,然後進行輸出並返回結果。

將 Promises 轉換成非同步函式

如果我們使用 Vanilla Promises 的話前面的示例將會是什麼樣?

這裡有相同的程式碼行數,但這是因為 then 和給它傳遞的回撥函式增加了很多的額外程式碼。另一個讓人厭煩的是兩個 return 關鍵字。這一直有些事困擾著我,因為它很難弄清楚使用 promises 的函式確切的返回是什麼。

就像你看到的,這個函式返回一個 promises,將會賦值給 val,猜一下生成器和非同步函式示例做了什麼!無論你在這個函式返回了什麼,你其實是暗地裡返回一個 promise 解析到那個值。如果你根本就沒有返回任何值,你暗地裡返回的 promise 解析為 undefined。

鏈式操作

Promise 之所以能受到眾人追捧,其中一個方面是因為它能以鏈式呼叫的方式把多個非同步操作連線起來,避免了嵌入形式的回撥。不過 async 函式在這個方面甚至比 Promise 做得還好。

下面演示瞭如何使用 Promise 來進行鏈式操作(我們只是簡單的多次執行 asynchronousOperation 來進行演示)。

使用 async 函式,只需要像編寫同步程式碼那樣呼叫 asynchronousOperation:

甚至最後的 return 語句中都不需要使用 await,因為用或不用,它都返回了包含了可處理終值的 Promise。

併發操作

Promise 還有另一個偉大的特性,它們可以同時進行多個非同步操作,等他們全部完成之後再繼續進行其它事件。ES2015 規範中提供了 Promise.all(),就是用來幹這個事情的。

這裡有一個示例:

Promise.all() 也可以當作 async 函式使用:

這裡就算使用了 Promise.all,程式碼仍然很清楚。

處理拒絕

Promises 可以被接受(resovled)也可以被拒絕(rejected)。被拒絕的 Promise 可以通過一個函式來處理,這個處理函式要傳遞給 then,作為其第二個引數,或者傳遞給 catch 方法。現在我們沒有使用 Promise API 中的方法,應該怎麼處理拒絕?可以通過 try 和 catch 來處理。使用 async 函式的時候,拒絕被當作錯誤來傳遞,這樣它們就可以通過 JavaScript 本身支援的錯誤處理程式碼來處理。

這與我們鏈式處理的示例非常相似,只是把它的最後一環改成了呼叫 catch。如果用 async 函式來寫,會像下面這樣。

它不像其它往 async 函式的轉換那樣簡潔,但是確實跟寫同步程式碼一樣。如果你在這裡不捕捉錯誤,它會延著呼叫鏈一直向上丟擲,直到在某處被捕捉處理。如果它一直未被捕捉,它最終會中止程式並丟擲一個執行時錯誤。Promise 以同樣的方式運作,只是拒絕不當作錯誤來處理;它們可能只是一個說明錯誤情況的字串。如果你不捕捉被建立為錯誤的拒絕,你會看到一個執行時錯誤,不過如果你只是使用一個字串,會失敗卻不會有輸出。

中斷 Promise

拒絕原生的 Promise,只需要使用 Promise 構建函式中的 reject 就好,當然也可以直接丟擲錯誤——在 Promise 的建構函式中,在 then 或 catch 的回撥中丟擲都可以。如果是在其它地方丟擲錯誤,Promise 就管不了了。

這裡有一些拒絕 Promise 的示例:

一般來說,最好使用 new Error,因為它會包含錯誤相關的其它資訊,比如丟擲位置的行號,以及可能會有用的呼叫棧。

這裡有一些丟擲 Promise 不能捕捉的錯誤的示例:

在 async 函式的 Promise 中丟擲錯誤就不會產生有關範圍的問題——你可以在 async 函式中隨時隨地丟擲錯誤,它總會被 Promise 抓住:

當然,我們永遠不會執行到 doAsyncOp 中的第二個錯誤,也不會執行到 return 語句,因為在那之前丟擲的錯誤已經中止了函式執行。

問題

如果你剛開始使用 async 函式,需要小心巢狀函式的問題。比如,如果你的 async 函式中有另一個函式(通常是回撥),你可能認為可以在其中使用 await ,但實際不能。你只能直接在 async 函式中使用 await 。

比如,這段程式碼無法執行:

第 4 行的 await 無效,因為它是在一個普通函式中使用的。不過可以通過為回撥函式新增 async 關鍵字來解決這個問題。

你看到它的時候會覺得理所當然,即便如此,仍然需要小心這種情況。

也許你還想知道等價的使用 Promise 的程式碼:

接下來的問題是關於把 async 函式看作同步函式。需要記住的是,async 函式內部的的程式碼是同步執行的,但是它會立即返回一個 Promise,並繼續執行外面的程式碼,比如:

你會看到 async 函式實際使用了內建的 Promise。這讓我們思考 async 函式中的同步行為,其它人可以通過普通的 Promise API 呼叫我們的 async 函式,也可以使用它們自己的 async 函式來呼叫。

如今,更好的非同步程式碼!

即使你本身不能使用非同步程式碼,你也可以進行編寫或使用工具將其編譯為 ES5。 非同步函式能讓程式碼更易於閱讀,更易於維護。 只要我們有 source maps,我們可以隨時使用更乾淨的 ES2017 程式碼。

有許多可以將非同步功能(和其他 ES2015+功能)編譯成 ES5 程式碼的工具。 如果您使用的是 Babel,這只是安裝 ES2017 preset 的例子。

相關文章