如何更好的編寫async函數

来源:https://www.cnblogs.com/jiasm/archive/2018/05/13/9031237.html
-Advertisement-
Play Games

2018年已經到了5月份,node的4.x版本也已經停止了維護 我司的某個服務也已經切到了8.x,目前正在做koa2.x的遷移 將之前的generator全部替換為async 但是,在替換的過程中,發現一些濫用async導致的時間上的浪費 所以來談一下,如何優化async代碼,更充分的利用非同步事件流 ...


2018年已經到了5月份,node4.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
  }
}

 

這樣的修改就會讓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
 

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1、Default模式,也是沒有設置緩存模式時的預設模式 這個模式實現http協議中的內容,比如響應碼是304時,當然還會結合E-Tag和LastModify等頭。 StringRequest request = new StringRequest(url, method); request.set ...
  • >>> JQuery 事件處理 >>> JQuery 事件處理 一、事件綁定方式 一、事件綁定方式 1、事件綁定的快捷方式: 缺點:綁定的事件,無法取消 2、使用on進行事件綁定 ① 使用on進行單事件綁定 ② 使用on,同時給多個事件綁定同一函數 ③ 使用on,同時給多個事件分別綁定不同的函數 ④ ...
  • 總述 我對於網頁這部分的理解吧,對於靜態網頁來說,無非分為三部分:第一部分、HTML,第二部分、CSS,第三部分、JavaScript(這部分暫且放一邊)。這倆個給我的第一印象就是,這你妹的都是一串串英文字母啊!一大堆的標簽,頭疼...但是如果我們仔細去想一下自己以前寫的這些英文字母,其實就好比在去 ...
  • 數組的2種創建方式: 1:數組直接量,在方括弧中將數組元素用逗號隔開即可。 數組直接量不一定要是常量,也可以是任意表達式。 數組也可以是包含對象直接量或者其他數組直接量 2,使用Array()構造函數 二維數組: 定義:整體上看是一個數組,一中一個元素又是一個數組,即數組中的數組。 數組的遍歷: 方 ...
  • 看書看到的一個jQuery小例子,分享給大家。(jquery引入可以選擇下載引入或者在jQuery官網找CDN) ...
  • 本文內容: 正則表達式 正則表達式的使用方法 正則表達式的特殊匹配字元 正則表達式修飾符 利用正則表達式進行表單驗證的例子 首發日期:2018-05-13 正則表達式: 正則表達式的使用方法: 首先創建正則表達式對象: 【正則表達式的字元規則:如果是沒有特殊意義的字元,直接寫;如果是有特殊意義的,直... ...
  • 最近需要處理十六進位,十進位,字元之間的轉換,所以去學習了進位數之間以及和字元之間的轉換,發現有很多差不多且書寫不正確的方法。自己也是查找文檔,一個一個實踐才真正清楚如何轉換,現在來記錄一下它們之間轉換的方法。 十六進位轉成十進位: convertedVal = parseInt(needConve ...
  • 開發中遇到一個需要優化的性能,頁面需要渲染很多table,而且可以自己添加table,所以就導致router改變時,清除這些DOM結構就會很慢,這就給用戶造成不好的體驗。 問題所在:清除渲染過多的DOM結構才導致遲緩; 解決方案:引入的table頁面作為一個子組件,存在切換table顯示內容的功能, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...