2018年已經到了5月份,node的4.x版本也已經停止了維護 我司的某個服務也已經切到了8.x,目前正在做koa2.x的遷移 將之前的generator全部替換為async 但是,在替換的過程中,發現一些濫用async導致的時間上的浪費 所以來談一下,如何優化async代碼,更充分的利用非同步事件流 ...
2018年已經到了5月份,
node
的4.x
版本也已經停止了維護
我司的某個服務也已經切到了8.x
,目前正在做koa2.x
的遷移
將之前的generator
全部替換為async
但是,在替換的過程中,發現一些濫用async
導致的時間上的浪費 所以來談一下,如何優化async
代碼,更充分的利用非同步事件流 杜絕濫用async
首先,你需要瞭解Promise
Promise
是使用async
/await
的基礎,所以你一定要先瞭解Promise
是做什麼的
Promise
是幫助解決回調地獄的一個好東西,能夠讓非同步流程變得更清晰。
一個簡單的Error-first-callback
轉換為Promise
的例子:
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
的處理。
async與Promise的關係
async
函數相當於一個簡寫的返回Promise
實例的函數,效果如下:
function getNumber () { return new Promise((resolve, reject) => { resolve(1) }) } // => async function getNumber () { return 1 }
兩者在使用上方式上完全一樣,都可以在調用getNumber
函數後使用then
進行監聽返回值。 以及與async
對應的await
語法的使用方式:
getNumber().then(data => { // got data }) // => let data = await getNumber()
await
的執行會獲取表達式後邊的Promise
執行結果,相當於我們調用then
獲取回調結果一樣。 P.S. 在async
/await
支持度還不是很高的時候,大家都會選擇使用generator
/yield
結合著一些類似於co
的庫來實現類似的效果
async函數代碼執行是同步的,結果返回是非同步的
async
函數總是會返回一個Promise
的實例 這點兒很重要
所以說調用一個async
函數時,可以理解為裡邊的代碼都是處於new Promise
中,所以是同步執行的
而最後return
的操作,則相當於在Promise
中調用resolve
:
async function getNumber () { console.log('call getNumber()') return 1 } getNumber().then(_ => console.log('resolved')) console.log('done') // 輸出順序: // call getNumber() // done // resolved
Promise內部的Promise會被消化
也就是說,如果我們有如下的代碼:
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 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 } }
這樣的修改就會讓getAvatar
與getUserInfo
內部的代碼同時執行,同時發送兩個請求,在外層通過包一層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()
這樣的執行好像並沒有什麼問題,我們也會得到1
、2
、3
三條log
的輸出,
但是當我們在await getUsersInfo()
下邊再添加一條console.log('done')
的話,就會發現:
我們會先得到done
,然後才是三條uid
的log
,也就是說,getUsersInfo
返回結果時,其實內部Promise
並沒有執行完。
這是因為forEach
並不會關心回調函數的返回值是什麼,它只是運行回調。
不要在普通的for、while迴圈中使用await
使用普通的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
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
函數編寫的幾個小提示:
- 使用
return Promise.reject()
在async
函數中拋出異常 - 讓相互之間沒有依賴關係的非同步函數同時執行
- 不要在迴圈的回調中/
for
、while
迴圈中使用await
,用map
來代替它
參考資料