2018年已經到了5月份,node
的4.x
版本也已經中止了維護
我司的某個服務也已經切到了8.x
,目前正在作koa2.x
的遷移
將以前的generator
所有替換爲async
可是,在替換的過程當中,發現一些濫用async
致使的時間上的浪費
因此來談一下,如何優化async
代碼,更充分的利用異步事件流 杜絕濫用async
Promise
是使用async
/await
的基礎,因此你必定要先了解Promise
是作什麼的 Promise
是幫助解決回調地獄的一個好東西,可以讓異步流程變得更清晰。
一個簡單的Error-first-callback
轉換爲Promise
的例子:javascript
const fs = require('fs') function readFile (fileName) { return new Promise((resolve, reject) => { fs.readFile(fileName, (err, data) => { if (err) reject(err) resolve(data) }) }) } readFile('test.log').then(data => { console.log('get data') }, err => { console.error(err) })
咱們調用函數返回一個Promise
的實例,在實例化的過程當中進行文件的讀取,當文件讀取的回調觸發式,進行Promise
狀態的變動,resolved
或者rejected
狀態的變動咱們使用then
來監聽,第一個回調爲resolve
的處理,第二個回調爲reject
的處理。html
async
函數至關於一個簡寫的返回Promise
實例的函數,效果以下:java
function getNumber () { return new Promise((resolve, reject) => { resolve(1) }) } // => async function getNumber () { return 1 }
二者在使用上方式上徹底同樣,均可以在調用getNumber
函數後使用then
進行監聽返回值。
以及與async
對應的await
語法的使用方式:node
getNumber().then(data => { // got data }) // => let data = await getNumber()
await
的執行會獲取表達式後邊的Promise
執行結果,至關於咱們調用then
獲取回調結果同樣。
P.S. 在async
/await
支持度還不是很高的時候,你們都會選擇使用generator
/yield
結合着一些相似於co
的庫來實現相似的效果git
async
函數老是會返回一個Promise
的實例 這點兒很重要
因此說調用一個async
函數時,能夠理解爲裏邊的代碼都是處於new Promise
中,因此是同步執行的
而最後return
的操做,則至關於在Promise
中調用resolve
:github
async function getNumber () { console.log('call getNumber()') return 1 } getNumber().then(_ => console.log('resolved')) console.log('done') // 輸出順序: // call getNumber() // done // resolved
也就是說,若是咱們有以下的代碼:koa
function getNumber () { return new Promise(resolve => { resolve(Promise.resolve(1)) }) } getNumber().then(data => console.log(data)) // 1
若是按照上邊說的話,咱們在then
裏邊獲取到的data
應該是傳入resolve
中的值 ,也就是另外一個Promise
的實例。
但實際上,咱們會直接得到返回值:1
,也就是說,若是在Promise
中返回一個Promise
,實際上程序會幫咱們執行這個Promise
,並在內部的Promise
狀態改變時觸發then
之類的回調。
一個有意思的事情:異步
function getNumber () { return new Promise(resolve => { resolve(Promise.reject(new Error('Test'))) }) } getNumber().catch(err => console.error(err)) // Error: Test
若是咱們在resolve
中傳入了一個reject
,則咱們在外部則能夠直接使用catch
監聽到。
這種方式常常用於在async
函數中拋出異常
如何在async
函數中拋出異常:async
async function getNumber () { return Promise.reject(new Error('Test')) } try { let number = await getNumber() } catch (e) { console.error(e) }
若是忘記添加await
關鍵字,代碼層面並不會報錯,可是咱們接收到的返回值倒是一個Promise
函數
let number = getNumber() console.log(number) // Promise
因此在使用時必定要切記await
關鍵字
let number = await getNumber() console.log(number) // 1
在代碼的執行過程當中,有時候,並非全部的異步都要添加await
的。
好比下邊的對文件的操做:
咱們假設fs
全部的API都被咱們轉換爲了Promise
版本
async function writeFile () { let fd = await fs.open('test.log') fs.write(fd, 'hello') fs.write(fd, 'world') return fs.close(fd) }
就像上邊說的,Promise內部的Promise會被消化,因此咱們在最後的close
也沒有使用await
咱們經過await
打開一個文件,而後進行兩次文件的寫入。
可是注意了,在兩次文件的寫入操做前邊,咱們並無添加await
關鍵字。
由於這是多餘的,咱們只須要通知API,我要往這個文件裏邊寫入一行文本,順序天然會由fs
來控制 。
最後再進行close
,由於若是咱們上邊在執行寫入的過程尚未完成時,close
的回調是不會觸發的,
也就是說,回調的觸發就意味着上邊兩步的write
已經執行完成了。
若是咱們如今要獲取一個用戶的頭像和用戶的詳細信息(而這是兩個接口 雖然說通常狀況下不太會出現)
async function getUser () { let avatar = await getAvatar() let userInfo = await getUserInfo() return { avatar, userInfo } }
這樣的代碼就形成了一個問題,咱們獲取用戶信息的接口並不依賴於頭像接口的返回值。
可是這樣的代碼卻會在獲取到頭像之後纔會去發送獲取用戶信息的請求。
因此咱們對這種代碼能夠這樣處理:
async function getUser () { let [avatar, userInfo] = await Promise.all([getAvatar(), getUserInfo()]) return { avatar, userInfo } }
這樣的修改就會讓getAvatar
與getUserInfo
內部的代碼同時執行,同時發送兩個請求,在外層經過包一層Promise.all
來確保二者都返回結果。
讓相互沒有依賴關係的異步函數同時執行
當咱們調用這樣的代碼時:
async function getUsersInfo () { [1, 2, 3].forEach(async uid => { console.log(await getUserInfo(uid)) }) } function getuserInfo (uid) { return new Promise(resolve => { setTimeout(_ => resolve(uid), 1000) }) } await getUsersInfo()
這樣的執行好像並無什麼問題,咱們也會獲得1
、2
、3
三條log
的輸出,
可是當咱們在await getUsersInfo()
下邊再添加一條console.log('done')
的話,就會發現:
咱們會先獲得done
,而後纔是三條uid
的log
,也就是說,getUsersInfo
返回結果時,其實內部Promise
並無執行完。
這是由於forEach
並不會關心回調函數的返回值是什麼,它只是運行回調。
使用普通的for
、while
循環會致使程序變爲串行:
for (let uid of [1, 2, 3]) { let result = await getUserInfo(uid) }
這樣的代碼運行,會在拿到uid: 1
的數據後纔會去請求uid: 2
的數據
目前最優的就是將其替換爲map
結合着Promise.all
來實現:
await Promise.all([1, 2, 3].map(async uid => await getUserInfo(uid)))
這樣的代碼實現會同時實例化三個Promise
,並請求getUserInfo
await*
,能夠省去Promise.all
await* [1, 2, 3].map(async uid => await getUserInfo(uid))
Generator
+co
時沒有這個問題在使用koa1.x
的時候,咱們直接寫yield [].map
是不會出現上述所說的串行問題的
看過co
源碼的小夥伴應該都明白,裏邊有這麼兩個函數(刪除了其他不相關的代碼):
function toPromise(obj) { if (Array.isArray(obj)) return arrayToPromise.call(this, obj); return obj; } function arrayToPromise(obj) { return Promise.all(obj.map(toPromise, this)); }
co
是幫助咱們添加了Promise.all
的處理的(膜拜TJ大佬)。
總結一下關於async
函數編寫的幾個小提示:
return Promise.reject()
在async
函數中拋出異常for
、while
循環中使用await
,用map
來代替它本人GitHub: jiasm 歡迎小夥伴們follow、交流