【譯】深刻理解 JavaScript 中的 Async and Await

原文:blog.bitsrc.io/understandi…
做者:Arfat Salman
翻譯:前端小白javascript

首先咱們來討論下回調函數,回調函數沒什麼特別的,只是在未來的某個時候執行的函數。因爲JavScript的異步特性,在許多不能當即得到結果的地方都須要回調前端

這是一個Node.js異步讀取文件的例子:java

fs.readFile(__filename, 'utf-8', (err, data) => {
  if (err) {
    throw err;
  }
  console.log(data);
});
複製代碼

當咱們想要執行多個異步操做時,就會出現問題。想象如下的場景(全部操做都是異步的):git

  • 咱們在數據庫中查詢用戶 Arfat。 咱們讀取 profile_img_url 並從 someServer.com 獲取圖像。
  • 獲取圖像後,咱們將其轉換爲另外一種格式,好比PNG到JPEG。
  • 若是轉換成功,咱們將向用戶發送電子郵件。
  • transformations.log 中記錄這個任務,並帶上時間戳

代碼大體以下:es6

回調地獄

注意回調函數的嵌套的末尾 }) 的層級關係,這種方式被戲稱做 回調地獄回調金字塔。缺點是 ——github

  • 代碼可讀性太差
  • 錯誤處理很複雜,經常致使錯誤代碼。

爲了解決上述問題,JavaScript 提出了 Promise。如今,咱們可使用鏈式結構而不是回調函數嵌套的結構。下面是一個例子 ——數據庫

使用promise

如今整個流程是自上而下而不是從左至右結構,這是一個優勢。可是 promise 仍然有一些缺點 ——json

  • 在每一個 .then 中咱們仍是要處理回調
  • 相比使用正常的 try/catch,咱們要使用 .catch() 處理錯誤
  • 在循環中按順序處理多個 promises 會很是頭疼,不直觀

咱們來演示下關於最後一個缺點:api

挑戰

假設咱們有一個for循環,它以隨機間隔(0到n秒)打印0到10。咱們須要使用 promise 按0到10順序打印出來。例如,若是0打印須要6秒,1打印須要2秒,那麼1應該等待0打印完以後再打印,以此類推。數組

不要使用 async/await 或者 .sort,以後咱們會解決這個問題

Async 函數

ES2017(ES8) 中引入了async函數,使promise應用起來很是簡單

  • 很重要的一點須要注意:async 函數的使用是基於 promise
  • 他們不是徹底不一樣的概念
  • 能夠認爲是一種基於 promise 異步代碼的替代方案
  • async/await 能夠避免使用 promise 鏈式調用
  • 代碼異步執行,看起來是同步式的

所以,理解 async/await 必需要先了解 promise

語法

async/await 包含兩個關鍵字 asyncawaitasync 用來使得函數能夠異步執行。async 可讓咱們在函數中使用 await ,除此以外,在任何地方使用 await 都屬於語法錯誤。

// With function declaration
async function myFn() {
  // await ...
}
// With arrow function
const myFn = async () => {
  // await ...
}
function myFn() {
  // await fn(); (Syntax Error since no async) 
}
複製代碼

注意,在函數聲明中 async 關鍵字在函數聲明的前面。在箭頭函數中,async 關鍵字則位於 = 和圓括號的中間。

async 函數還能做爲對象的方法,或是在類的聲明中。

// As an object's method
const obj = {
  async getName() {
    return fetch('https://www.example.com');
  }
}
// In a class
class Obj {
  async getResource() {
    return fetch('https://www.example.com');
  }
}
複製代碼

注意:類的構造函數和 getters/setters 不能使用 async 函數。

語義和評估準則

async是普通的JavaScript函數,它們有如下不一樣之處--

async 函數老是返回 promise 對象。

async function fn() {
  return 'hello';
}
fn().then(console.log)
// hello
複製代碼

函數 fn 返回 'hello',因爲咱們使用了 async 關鍵字, 'hello' 被包裝成了一個 promise 對象(經過 Promise 構造函數實現)

這是另一種實現方式,不使用 async

function fn() {
  return Promise.resolve('hello');
}
fn().then(console.log);
// hello
複製代碼

上面代碼中,咱們手動返回了一個 promise 對象,沒有使用 async 關鍵字

更準確地說,async函數的返回值會被 Promise.resolve 包裹。

若是返回值是原始值,Promise.resolve 會返回一個promise化的值,若是返回值是一個promise對象,則直接返回這個對象

// in case of primitive values
const p = Promise.resolve('hello')
p instanceof Promise; 
// true
// p is returned as is it
Promise.resolve(p) === p; 
// true
複製代碼

若是async函數中拋出錯誤怎麼辦?

好比--

async function foo() {
  throw Error('bar');
}
foo().catch(console.log);
複製代碼

若是錯誤未被捕獲,foo() 函數會返回一個狀態爲 rejectedpromise。不一樣於 Promise.resolvePromise.reject 會包裹錯誤並返回。詳情請看稍後的錯誤處理部分。

最終的結果是,不管你返回什麼結果,你都將從async函數中獲得一個promise。

async 函數遇到 await <表達式>時會暫停

await做用於一個表達式。當表達式是一個promise時,async函數會暫停執行,直到該promise狀態變爲 resolved。當表達式爲非promise值時,會使用 Promise.resolve 將其轉換爲promise,而後狀態變爲 resolved

// utility function to cause delay
// and get random value
const delayAndGetRandom = (ms) => {
  return new Promise(resolve => setTimeout(
    () => {
      const val = Math.trunc(Math.random() * 100);
      resolve(val);
    }, ms
  ));
};
async function fn() {
  const a = await 9;
  const b = await delayAndGetRandom(1000);
  const c = await 5;
  await delayAndGetRandom(1000);
  
  return a + b * c;
}
// Execute fn
fn().then(console.log);
複製代碼

讓咱們來逐行看看 fn 函數

  • 當函數執行時,第一行代碼 const a = await 9,內部會被解析爲 const a = await Promise.resolve(9)
  • 由於使用到了 await, 因此函數執行會暫停,直到變量 a 獲得一個值,在promise 會將其resolve爲9
  • delayAndGetRandom(1000) 會使 fn 函數暫停,直到1秒鐘以後 delayAndGetRandom 被resolve,因此,fn 函數的執行有效地暫停了 1 秒鐘
  • 此外,delayAndGetRandom 返回一個隨機數。不管在resolve函數中傳遞什麼,它都被分配給變量 b
  • 一樣,變量 c 值爲 5 ,而後使用 await delayAndGetRandom(1000) 又延時了 1 秒鐘。在這行代碼中咱們並無使用 Promise.resolve 返回值。
  • 最後,咱們計算 a + b * c 結果,並用 Promise.resolve 包裹並返回

注意:若是這裏函數的暫停和恢復使你想起了 ES6 generators ,那是由於 generator 有不少 優勢

解決方案

讓咱們用 async/await 來解決文章開頭提出一個假設問題:

使用 async/await

咱們定義了一個 async 函數 finishMyTask,使用 await 等待 queryDatabase, sendEmail, logTaskInFile 函數執行返回結果

若是咱們將 async/await 解決方案與使用 promise 的方案進行對比,會發現代碼的數量很相近。可是 async/await 使得代碼看起來更簡單,不用去記憶多層回調函數以及 .then /.catch

如今讓咱們來解決關於打印數字的問題,有兩種解決方法

const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));

// Implementation One (Using for-loop)
const printNumbers = () => new Promise((resolve) => {
  let pr = Promise.resolve(0);
  for (let i = 1; i <= 10; i += 1) {
    pr = pr.then((val) => {
      console.log(val);
      return wait(i, Math.random() * 1000);
    });
  }
  resolve(pr);
});

// Implementation Two (Using Recursion)

const printNumbersRecursive = () => {
  return Promise.resolve(0).then(function processNextPromise(i) {

    if (i === 10) {
      return undefined;
    }

    return wait(i, Math.random() * 1000).then((val) => {
      console.log(val);
      return processNextPromise(i + 1);
    });
  });
};
複製代碼

若是使用async函數,更簡單

async function printNumbersUsingAsync() {
  for (let i = 0; i < 10; i++) {
    await wait(i, Math.random() * 1000);
    console.log(i);
  }
}
複製代碼

錯誤處理

咱們在語法部分所瞭解的,一個未捕獲的 Error() 會被包裝在一個 rejected promise 中,可是,咱們能夠在 async 函數中同步地使用 try-catch 處理錯誤。讓咱們從這一實用的函數開始 ——

async function canRejectOrReturn() {
  // wait one second
  await new Promise(res => setTimeout(res, 1000));
// Reject with ~50% probability
  if (Math.random() > 0.5) {
    throw new Error('Sorry, number too big.')
  }
return 'perfect number';
}
複製代碼

canRejectOrReturn() 是一個異步函數,要麼 resolve 'perfect number',要麼 reject Error('Sorry, number too big')

看看下面的代碼

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}
複製代碼

由於咱們在等待 canRejectOrReturn 執行,它的 rejection 會被轉換爲一個錯誤拋出,catch 會執行,也就是說 foo 函數結果要麼 resolve 爲 undefined(由於咱們在 try 中沒有返回值),要麼 resolve 'error caught'。由於咱們在 foo 函數中使用了 try-catch 處理錯誤,因此說 foo 函數的結果永遠不會是 rejected。

另外一個例子

async function foo() {
  try {
    return canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}
複製代碼

注意。此次在 foo 函數裏面咱們返回而不是等待 canRejectOrReturnfoo 要麼 resolve 'perfect number',要麼 reject Error('Sorry, number too big')catch 語句不會被執行

由於咱們 returncanRejectOrReturn 返回的 promise 對象,所以 foo 最終的狀態由 canRejectOrReturn 的狀態決定,你能夠將 return canRejectOrReturn() 分紅兩行代碼,來更清楚的瞭解,注意第一行沒有 await

try {
    const promise = canRejectOrReturn();
    return promise;
}
複製代碼

讓咱們來看看 returnawait 一塊兒使用的狀況

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}
複製代碼

在這種狀況下 foo resolve 'perfect number',或者 resolve 'error caught',沒有 rejection,就像上面那個只有 await 的例子,在這裏咱們 resolve 了 canRejectOrReturn 返回的值,而不是 undefined

比也能夠將 return await canRejectOrReturn() 拆分來看

try {
    const value  = await canRejectOrReturn();
    return value;
}
// ...
複製代碼

常見的錯誤和陷阱

因爲 Promise 和 async/await 之間錯綜複雜的操做。 可能會有一些隱藏的錯誤,咱們來看看 -

沒有使用 await

有時候咱們會忘記在 promise 前面使用 await,或者忘記 return

async function foo() {
  try {
    canRejectOrReturn();
  } catch (e) {
    return 'caught';
  }
}
複製代碼

注意,若是咱們不使用 await 或者 returnfoo 老是會 resolve undefined,不會等待一秒,可是 canRejectOrReturn() 中的 promise 的確被執行了。若是有反作用,也會產生,若是拋出錯誤或者 reject,UnhandledPromiseRejectionWarning 就會產生

在回調中使用 async 函數

咱們常常在 .map.filter 中使用 async 函數做爲回調函數,假設咱們有一個函數 fetchPublicReposCount(username),能夠獲取一個 github 用戶擁有的公開倉庫的數量。咱們想要得到三名不一樣用戶的公開倉庫數量,讓咱們來看代碼 —

const url = 'https://api.github.com/users';
// Utility fn to fetch repo counts
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}
複製代碼

咱們想要獲取 ['ArfatSalman', 'octocat', 'norvig'] 三我的的倉庫數量,會這樣作:

const users = [
  'ArfatSalman',
  'octocat',
  'norvig'
];
const counts = users.map(async username => {
  const count = await fetchPublicReposCount(username);
  return count;
});
複製代碼

注意 async.map 的回調函數中,咱們但願 counts 變量包含倉庫數量,然而就像咱們以前看到,async 函數會返回一個 promise 對象,所以 counts 其實是一個 promises 數組,這個數組包含着每次調用函數獲取用戶倉庫數量返回的 promise,

過分按順序使用 await

async function fetchAllCounts(users) {
  const counts = [];
  for (let i = 0; i < users.length; i++) {
    const username = users[i];
    const count = await fetchPublicReposCount(username);
    counts.push(count);
  }
  return counts;
}
複製代碼

咱們手動獲取了每個 count,並將它們保存到 counts 數組中。程序的問題在於第一個用戶的 count 被獲取以後,第二個用戶的 count 才能被獲取。同一時間,只能獲取一個倉庫的數量。

若是一個 fetch 操做耗時 300 ms,那麼 fetchAllCounts 函數耗時大概在 900 ms 左右。因而可知,程序耗時會隨着用戶數量的增長而線性增長。由於獲取不一樣用戶公開倉庫數量之間沒有依賴關係,因此咱們能夠將操做並行處理。

咱們能夠同時獲取用戶,而不是按順序執行。 咱們將使用 .mapPromise.all

async function fetchAllCounts(users) {
  const promises = users.map(async username => {
    const count = await fetchPublicReposCount(username);
    return count;
  });
  return Promise.all(promises);
}
複製代碼

Promise.all 接受一個 promise 對象數組做爲輸入,返回一個 promise 對象做爲輸出。當全部 promise 對象的狀態都轉變成 resolved 時,返回值是一個包含全部結果的數組,而失敗的時候則返回最早被reject失敗狀態的值,只要有一個 promise 對象被 rejected,Promise.all 的返回值爲第一個被 rejected 的 promise 對象對應的返回值。可是,同時運行全部 promise 可能行不通。若是你想批量完成 promise。能夠參考 p-map 關於數量可控的併發操做。

總結

async 函數很是重要。隨着 Async Iterators 的引入,async 函數將會應用得愈來愈廣。對於現代 JavaScript 開發人員來講掌握並理解 async 函數相當重要。但願這篇文章能對你有所幫助

相關文章
相關標籤/搜索