Hey async, await me

發表於2015-12-24

背景

筆者在前面的文章介紹過如何使用generator來解決callback hell,儘管現在多數瀏覽器特別是移動端瀏覽器還不支援該ES2015新特性,但你可以通過Babel等轉換工具轉化成ES5相容的等效程式碼,從而在生產環境使用。

不過使用generator來解決callback hell似乎有點不務正業,畢竟generator是生成器,屬於Iterator的一種,設計之初是用來生成一種特殊的迭代器的。

另外還有兩點也可以算是generator解決callback hell問題的缺陷:

  1. generator需要從generator function執行得到,而generator function執行之後只會返回一個generator,不管裡面是怎樣的程式碼,與我們通常對函式的認知存在差異
  2. 如果想執行generator function的函式體,需要不斷呼叫返回的generator的next方法,這樣就決定了必須依賴cobluebird.coroutine等其他輔助程式碼或者手動執行next,來保證generator不斷next下去

Tips:文章ES6 Generator介紹有介紹generator和generator function,以及它們之間的關係和區別。

 

眾所周知,ES2015來的太晚了,而現在,TC39決定加快腳步,也許每年都會有新版本釋出,明年可能會發布ES2016。ES2016終於給JS帶來了async/await原生支援,而其他語言如C#、Python等更早就支援上了。

而async/await正是本文要重點介紹的用來解決callback hell問題的終極大殺器。 雖然離瀏覽器或nodejs支援ES2016還有很久很久,但依靠babel任然可以轉換出當前環境就支援的程式碼。

本文的最後還將分享筆者在生產環境使用async/await的經驗,對,就是生產環境。

async/await語法

函式宣告

函式表示式

匿名函式

箭頭函式

類方法

沒什麼特別的,就在我們通常的寫法前加上關鍵字async就行了,就像generator function僅僅比普通function多了一個*。

function前面加上async關鍵字,表示該function需要執行非同步程式碼。 async function函式體內可以使用await關鍵字,且await關鍵字只能出現在async function函式體內,這一點和generator function跟yield的關係一樣。

await關鍵字可以跟在任意變數或者表示式之前,從字面很好理解該關鍵字有等待的意思,所以更有價值的用法是await後面跟一個非同步過程,通常是Promise,

如果用generator來解決callback hell,必須配合使用yield關鍵字和next方法,而理解清楚yield的作用和返回值以及next的引數作用就夠消化兩天了,await關鍵字不像yield關鍵字和next方法這麼難以理解,它的意思就是等待,作用也是等待,而且一個關鍵字就夠了。

Tips:前文介紹yield的時候還提到了yield*,其實ES2016草案裡面也提到了await*,不過它不是標準的一部分,草案並不要求必須實現,而且草案並不建議使用,不過後文還是會提到await*的用法。

做正確的事

用generator來解決非同步函式回撥問題始終覺得有些彆扭,現在就讓它做回本職工作吧,回撥問題就交由async/await來解決——做正確的事。

先來回顧一下generator配合co來解決非同步回撥問題的方法,首先yy一個場景,見註釋

再來看看async/await的解決方式

Tips:程式碼片中用到了一些ES2015的新語法,不要介意,隨便查一些文件就能看懂。

可以看到兩種方法在程式碼的寫法上非常相似,不嚴格的說,僅僅將function*換成async function,同時將函式體裡面的yield關鍵字換成await關鍵字即可,順便還可以把co等輔助工具拋棄了。

那麼代價,哦不,好處是什麼?

  1. 更接近自然語言,async/await比function*/yield更好理解,需要非同步執行的函式加一個標記async,呼叫的時候在前面加一個await,表示需要等到非同步函式返回了才執行下面的語句
  2. 無需依賴其他輔助程式碼,js原生能力支援
  3. event listener、大量函式的callback等,不支援generator function,但是支援async function(所有支援普通function的地方都支援async function),無需co.wrap等輔助程式碼來包裝
  4. 在某些JS引擎執行generator function的bind方法,會返回一個普通function,儘管這是引擎的問題,async function不存在這樣的問題,bind之後還是返回一個async function,從而可以避免一些意想不到的問題

async function的返回值

值得注意的是,和generator function固定會返回一個generator類似,async function固定會返回一個promise,不管函式體裡面有沒有顯示呼叫return。

如果有return,return後面的值都會被包裝成一個promise,所以return ‘hello world’和return Promise.resolve(‘hello world’)其實是一樣的效果。

由於async function返回一個promise,我們可以跟在await後面,類似這樣

其實和下面的程式碼是一樣的效果

這樣就達到多個非同步函式序列執行的目的了,看起來就跟同步函式一樣。

await*

多個非同步函式,有了序列執行的能力,自然也需要有並行執行的能力。

generator的方式

Tips:不是yield*

async的方式

等效於

不過草案並不推薦await*,以後的瀏覽器也不一定會實現這種語法,還是推薦使用Promise.all的方式,不過babel等轉換工具是支援await*的。

在React中使用async/await

前文提過,筆者已在生產環境用過async function了, 當前React正火的不要不要的,前段時間正好藉此機會用React搭了個內部使用的系統, 以展示個人資訊(info)元件為例

個人資訊需要發起後臺請求才能得到,一般的做法是在getInitialState的時候返回一個初始值info,然後在componentDidMount裡發起網路請求,得到info,再更新state,重新渲染元件。

Tips:使用箭頭函式可以避免this錯亂的問題,你肯定寫過下面這樣的程式碼

雖然async function的返回值一定是一個promise,然而我們並不關心componentDidMount的返回值,所以可以將一個async function賦值給componentDidMount,一切都會按照預期執行。

Tips:沒有閉包,沒有作用域變化,可以放心使用this,錯誤處理直接使用try/catch

最後一步

使用babel(配合構建工具或者單獨使用babel-cli)將程式碼轉換成相容ES5的等效程式碼,本文不講怎麼使用babel,官網有詳盡的教程。

如你所願,在React中使用async/await就這麼簡單。

總結

  • async/await才是解決非同步回撥的最佳實踐,終於可以放歸generator了
  • async/await只是一套語法糖,其他語言的async/await可能是協程或者多執行緒程式設計的語法糖,JS本身是單執行緒的,async/await與傳統的callback或者promise執行起來並無兩樣
  • 當下的JS引擎還沒有原生支援async/await的,不過現在就可以使用babel轉換成ES5等效程式碼,你甚至可以在生產環境中使用
  • 雖然async/await是ES2016才支援的新特性,目前尚處於草案狀態,不過其作用和用法基本不會變了,一些其他語言已實現該特性,看來確實是大勢所趨

相關文章