背景
筆者在前面的文章介紹過如何使用generator來解決callback hell,儘管現在多數瀏覽器特別是移動端瀏覽器還不支援該ES2015新特性,但你可以通過Babel等轉換工具轉化成ES5相容的等效程式碼,從而在生產環境使用。
不過使用generator來解決callback hell似乎有點不務正業,畢竟generator是生成器,屬於Iterator的一種,設計之初是用來生成一種特殊的迭代器的。
另外還有兩點也可以算是generator解決callback hell問題的缺陷:
- generator需要從generator function執行得到,而generator function執行之後只會返回一個generator,不管裡面是怎樣的程式碼,與我們通常對函式的認知存在差異
- 如果想執行generator function的函式體,需要不斷呼叫返回的generator的next方法,這樣就決定了必須依賴co或bluebird.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語法
函式宣告
1 |
async function asyncFunc() {} |
函式表示式
1 |
const asyncFunc = async function() {} |
匿名函式
1 |
async function() {} |
箭頭函式
1 |
async () => {} |
類方法
1 2 3 |
Class someClass { async asyncFunc() {} } |
沒什麼特別的,就在我們通常的寫法前加上關鍵字async就行了,就像generator function僅僅比普通function多了一個*。
function前面加上async關鍵字,表示該function需要執行非同步程式碼。 async function函式體內可以使用await關鍵字,且await關鍵字只能出現在async function函式體內,這一點和generator function跟yield的關係一樣。
1 2 3 |
async function asyncFunc() { await anything; } |
await關鍵字可以跟在任意變數或者表示式之前,從字面很好理解該關鍵字有等待的意思,所以更有價值的用法是await後面跟一個非同步過程,通常是Promise,
1 2 3 |
async function asyncFunc() { await somePromise; } |
如果用generator來解決callback hell,必須配合使用yield關鍵字和next方法,而理解清楚yield的作用和返回值以及next的引數作用就夠消化兩天了,await關鍵字不像yield關鍵字和next方法這麼難以理解,它的意思就是等待,作用也是等待,而且一個關鍵字就夠了。
Tips:前文介紹yield的時候還提到了yield*,其實ES2016草案裡面也提到了await*,不過它不是標準的一部分,草案並不要求必須實現,而且草案並不建議使用,不過後文還是會提到await*的用法。
做正確的事
用generator來解決非同步函式回撥問題始終覺得有些彆扭,現在就讓它做回本職工作吧,回撥問題就交由async/await來解決——做正確的事。
先來回顧一下generator配合co來解決非同步回撥問題的方法,首先yy一個場景,見註釋
1 2 3 4 5 6 7 8 9 10 11 12 |
co(*() => { try { // 獲取使用者名稱 const name = yield $.ajax('get_my_name'); // 根據使用者名稱獲取個人資訊 const info = yield $.ajax(`get_my_info_by_name'?name=${name}`); // 列印個人資訊 console.log(info); } catch(err) { console.error(err); } }); |
再來看看async/await的解決方式
1 2 3 4 5 6 7 8 9 10 11 12 |
(async () => { try { // 獲取使用者名稱 const name = await $.ajax('get_my_name'); // 根據使用者名稱獲取個人資訊 const info = await $.ajax(`get_my_info_by_name'?name=${name}`); // 列印個人資訊 console.log(info); } catch(err) { console.error(err); } })(); |
Tips:程式碼片中用到了一些ES2015的新語法,不要介意,隨便查一些文件就能看懂。
可以看到兩種方法在程式碼的寫法上非常相似,不嚴格的說,僅僅將function*換成async function,同時將函式體裡面的yield關鍵字換成await關鍵字即可,順便還可以把co等輔助工具拋棄了。
那麼代價,哦不,好處是什麼?
- 更接近自然語言,async/await比function*/yield更好理解,需要非同步執行的函式加一個標記async,呼叫的時候在前面加一個await,表示需要等到非同步函式返回了才執行下面的語句
- 無需依賴其他輔助程式碼,js原生能力支援
- event listener、大量函式的callback等,不支援generator function,但是支援async function(所有支援普通function的地方都支援async function),無需co.wrap等輔助程式碼來包裝
- 在某些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後面,類似這樣
1 2 3 4 5 6 7 8 |
async function asyncFun1() {} async function asyncFun2() { await asyncFun1(); } async function asyncFun3() { await asyncFun2(); } asyncFun3(); |
其實和下面的程式碼是一樣的效果
1 2 3 4 5 6 7 |
async function asyncFun1() {} async function asyncFun2() {} async function asyncFun3() { await asyncFun1(); await asyncFun2(); } asyncFun3(); |
這樣就達到多個非同步函式序列執行的目的了,看起來就跟同步函式一樣。
await*
多個非同步函式,有了序列執行的能力,自然也需要有並行執行的能力。
generator的方式
1 |
yield [promise1, promise2, ..., promisen] |
Tips:不是yield*
async的方式
1 |
await* [promise1, promise2, ..., promisen] |
等效於
1 |
await Promise.all([promise1, promise2, ..., promisen]) |
不過草案並不推薦await*,以後的瀏覽器也不一定會實現這種語法,還是推薦使用Promise.all的方式,不過babel等轉換工具是支援await*的。
在React中使用async/await
前文提過,筆者已在生產環境用過async function了, 當前React正火的不要不要的,前段時間正好藉此機會用React搭了個內部使用的系統, 以展示個人資訊(info)元件為例
個人資訊需要發起後臺請求才能得到,一般的做法是在getInitialState的時候返回一個初始值info,然後在componentDidMount裡發起網路請求,得到info,再更新state,重新渲染元件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
React.createClass({ getInitialState() { return {info: {}}; }, componentDidMount() { // 獲取使用者名稱 $.ajax('get_my_name') .then(name => { // 根據使用者名稱獲取個人資訊 // 鏈式Promise return $.ajax(`get_my_info_by_name'?name=${name}`); }).then(info => { this.setSate({info}); }).catch(err => { console.error(err); }); }, render() { // render info } }); |
Tips:使用箭頭函式可以避免this錯亂的問題,你肯定寫過下面這樣的程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
componentDidMount() { const self = this; // 獲取使用者名稱 $.ajax('get_my_name') .then(name => { // 根據使用者名稱獲取個人資訊 // 鏈式Promise return $.ajax(`get_my_info_by_name'?name=${name}`); }).then(function(info) { self.setSate({info}); }).catch(function(err) => { console.error(err); }); } |
雖然async function的返回值一定是一個promise,然而我們並不關心componentDidMount的返回值,所以可以將一個async function賦值給componentDidMount,一切都會按照預期執行。
1 2 3 4 5 6 7 8 9 10 11 |
async componentDidMount() { try { // 獲取使用者名稱 const name = await $.ajax('get_my_name'); // 根據使用者名稱獲取個人資訊 const info = await $.ajax(`get_my_info_by_name'?name=${name}`); this.setSate({info}); } catch(err) { console.error(err); } } |
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才支援的新特性,目前尚處於草案狀態,不過其作用和用法基本不會變了,一些其他語言已實現該特性,看來確實是大勢所趨