如何更好的編寫async函數

2018年已經到了5月份, node4.x版本也已經中止了維護
我司的某個服務也已經切到了 8.x,目前正在作 koa2.x的遷移
將以前的 generator所有替換爲 async
可是,在替換的過程當中,發現一些濫用 async致使的時間上的浪費
因此來談一下,如何優化 async代碼,更充分的利用異步事件流 杜絕濫用async

首先,你須要瞭解Promise

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的關係

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函數代碼執行是同步的,結果返回是異步的

async函數老是會返回一個Promise的實例 這點兒很重要
因此說調用一個async函數時,能夠理解爲裏邊的代碼都是處於new Promise中,因此是同步執行的
而最後return的操做,則至關於在Promise中調用resolvegithub

async function getNumber () {
  console.log('call getNumber()')

  return 1
}

getNumber().then(_ => console.log('resolved'))
console.log('done')

// 輸出順序:
// call getNumber()
// done
// resolved

Promise內部的Promise會被消化

也就是說,若是咱們有以下的代碼: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關鍵字

若是忘記添加await關鍵字,代碼層面並不會報錯,可是咱們接收到的返回值倒是一個Promise函數

let number = getNumber()
console.log(number) // Promise

因此在使用時必定要切記await關鍵字

let number = await getNumber()
console.log(number) // 1

不是全部的地方都須要添加await

在代碼的執行過程當中,有時候,並非全部的異步都要添加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函數調用

若是咱們如今要獲取一個用戶的頭像和用戶的詳細信息(而這是兩個接口 雖然說通常狀況下不太會出現

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
  }
}

這樣的修改就會讓getAvatargetUserInfo內部的代碼同時執行,同時發送兩個請求,在外層經過包一層Promise.all來確保二者都返回結果。

讓相互沒有依賴關係的異步函數同時執行

一些循環中的注意事項

forEach

當咱們調用這樣的代碼時:

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()

這樣的執行好像並無什麼問題,咱們也會獲得123三條log的輸出,
可是當咱們在await getUsersInfo()下邊再添加一條console.log('done')的話,就會發現:
咱們會先獲得done,而後纔是三條uidlog,也就是說,getUsersInfo返回結果時,其實內部Promise並無執行完。
這是由於forEach並不會關心回調函數的返回值是什麼,它只是運行回調。

不要在普通的for、while循環中使用await

使用普通的forwhile循環會致使程序變爲串行:

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

P.S. 草案中有一個await*,能夠省去Promise.all

await* [1, 2, 3].map(async uid => await getUserInfo(uid))

P.S. 爲何在使用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函數編寫的幾個小提示:

  1. 使用return Promise.reject()async函數中拋出異常
  2. 讓相互之間沒有依賴關係的異步函數同時執行
  3. 不要在循環的回調中/forwhile循環中使用await,用map來代替它

參考資料

  1. async-function-tips
本人GitHub: jiasm 歡迎小夥伴們follow、交流
相關文章
相關標籤/搜索