如何正確使用async/await?

infoq發表於2018-08-01

  ES7引入的async/await是JavaScript非同步程式設計的一個重大改進,提供了在不阻塞主執行緒的情況下使用同步程式碼非同步訪問資源的能力。在本文中,我們將從不同的角度探索async/await,並演示如何正確有效地使用它們。

 async/await的好處

  async/await給我們帶來的最重要的好處是同步程式設計風格。我們來看一個例子。

// async/await
async getBooksByAuthorWithAwait(authorId) {
  const books = await bookModel.fetchAll();
  return books.filter(b => b.authorId === authorId);
}
// promise
getBooksByAuthorWithPromise(authorId) {
  return bookModel.fetchAll()
    .then(books => books.filter(b => b.authorId === authorId));
}

  很顯然,async/await比promise更容易理解。如果忽略掉await關鍵字,程式碼看起來與其他任意一門同步語言一樣(如Python)。

  除了可讀性,async/await還對瀏覽器提供了原生支援。目前所有的主流瀏覽器都完全支援非同步功能。



  原生支援意味著不需要編譯程式碼。更重要的是,它除錯起來很方便。在函式入口設定斷點並執行跳過await行之後,偵錯程式會在bookModel.fetchAll()執行時暫停一會兒,然後移動到下一行(也就是.filter)!這比使用promise要容易除錯得多,因為你必須在.filter這一行設定另一個斷點。



  另一個好處是async關鍵字,儘管看起來不是很明顯。它宣告getBooksByAuthorWithAwait()函式的返回值是一個promise,因此呼叫者可以安全地呼叫getBooksByAuthorWithAwait().then(…)或await getBooksByAuthorWithAwait()。比如像下面這段程式碼:

getBooksByAuthorWithPromise(authorId) {
  if (!authorId) {
    return null;
  }
  return bookModel.fetchAll()
    .then(books => books.filter(b => b.authorId === authorId));
  }
}

  在上面的程式碼中,getBooksByAuthorWithPromise可能返回一個promise(正常情況)或null(異常情況),在這種情況下,呼叫者無法安全地呼叫.then()。而如果使用async宣告,則不會出現這種情況。

 async/await可能會引起誤解

  有些文章將async/await與promise進行了比較,並聲稱它是JavaScript非同步程式設計演變的下一代,但我非常不同意這一觀點。async/await是一種改進,但它不過是一種語法糖,它不會完全改變我們的程式設計風格。

  從本質上講,非同步函式仍然是promise。在正確使用非同步函式之前,你必須瞭解promise,更糟糕的是,大部分時間需要同時使用promise和非同步函式。

  考慮上例中的getBooksByAuthorWithAwait()和getBooksByAuthorWithPromises()函式。請注意,它們不僅功能相同,介面也是完全一樣的!

  這意味著如果直接呼叫getBooksByAuthorWithAwait(),它將返回一個promise。

  不過這不一定是件壞事。只是await會給人一種感覺:“它可以將非同步函式轉換為同步函式”。但這實際上是錯誤的。

 async/await的陷阱

  那麼人們在使用async/await時可能會犯什麼錯誤?下面列舉了一些常見的錯誤。

  太過序列化

  雖然await可以讓你的程式碼看起來像是同步的,但請記住,它們仍然是非同步的,要避免太過序列化。

async getBooksAndAuthor(authorId) {
  const books = await bookModel.fetchAll();
  const author = await authorModel.fetch(authorId);
  return {
    author,
    books: books.filter(book => book.authorId === authorId),
  };
}

  上面的程式碼在邏輯上看起來很正確,但這樣做其實是不對的。

  1. await bookModel.fetchAll()將等到fetchAll()返回。
  2. 然後await authorModel.fetch(authorId)將被呼叫。

  注意,authorModel.fetch(authorId)不依賴bookModel.fetchAll()的結果,事實上它們可以並行呼叫!然而,因為在這裡使用了await,兩個呼叫變成序列的,總的執行時間將比並行版本要長得多。

  正確的方法應該是:

async getBooksAndAuthor(authorId) {
  const bookPromise = bookModel.fetchAll();
  const authorPromise = authorModel.fetch(authorId);
  const book = await bookPromise;
  const author = await authorPromise;
  return {
    author,
    books: books.filter(book => book.authorId === authorId),
  };
}

  或者更糟糕的是,如果你想要逐個獲取物品清單,你必須使用promise:

async getAuthors(authorIds) {
  // WRONG, this will cause sequential calls
  // const authors = _.map(
  //   authorIds,
  //   id => await authorModel.fetch(id));
// CORRECT
  const promises = _.map(authorIds, id => authorModel.fetch(id));
  const authors = await Promise.all(promises);
}

  總之,你仍然需要將流程視為非同步的,然後使用await寫出同步的程式碼。在複雜的流程中,直接使用promise可能更方便。

 錯誤處理

  在使用promise時,非同步函式有兩個可能的返回值。對於正常情況,可以使用.then(),而對於異常情況,則使用.catch()。不過在使用async/await時,錯誤處理可能會變得有點蹊蹺。

  try…catch

  最標準的(也是我推薦的)方法是使用try…catch語句。在呼叫await函式時,如果出現非正常狀況就會跑出異常。比如:

class BookModel {
  fetchAll() {
    return new Promise((resolve, reject) => {
      window.setTimeout(() => { reject({'error': 400}) }, 1000);
    });
  }
}
// async/await
async getBooksByAuthorWithAwait(authorId) {
try {
  const books = await bookModel.fetchAll();
} catch (error) {
  console.log(error);    // { "error": 400 }
}

  在捕捉到異常之後,我們有幾種方法來處理它:

  • 處理異常,並返回一個正常值。(不在catch塊中使用任何return語句相當於使用return undefined,undefined也是一個正常值。)
  • 如果你想讓呼叫者來處理它,就將它丟擲。你可以直接丟擲錯誤物件,比如throw error,這樣就可以在promise鏈中使用await getBooksByAuthorWithAwait()函式(也就是像getBooksByAuthorWithAwait().then(...).catch(error => …)這樣呼叫它)。或者你可以將錯誤包裝成Error物件,比如throw new Error(error),那麼在控制檯中顯示這個錯誤時它將給出完整的堆疊跟蹤資訊。
  • 拒絕它,比如return Promise.reject(error)。這相當於throw error,因此不推薦使用。

  使用try…catch的好處是:

  • 簡單,傳統。只要你有其他語言(如Java或C++)的程式設計經驗,要理解這一點就不會有任何困難。
  • 如果沒有必要逐步進行錯誤處理,那麼可以在單個try…catch塊中包裝多個await呼叫,這樣就可以在一個地方處理所有錯誤。

  這種方法也有一個缺陷。由於try...catch會捕獲程式碼塊中的每個異常,所以通常不會被promise捕獲的異常也會被捕獲到。比如:

class BookModel {
  fetchAll() {
    cb();    // note `cb` is undefined and will result an exception
    return fetch('/books');
  }
}
try {
  bookModel.fetchAll();
} catch(error) {
  console.log(error);  // This will print "cb is not defined"
}

  執行此程式碼,你將會在控制檯看到“ReferenceError:cb is not defined”錯誤,訊息的顏色是黑色的。錯誤訊息是通過console.log()輸出的,而不是JavaScript本身。有時候這可能是致命的:如果BookModel被包含在一系列函式呼叫中,並且其中一個呼叫把錯誤吞噬掉了,那麼找到這樣的undefined錯誤將非常困難。

 讓函式返回兩個值

  錯誤處理的另一種方式是受到了Go語言啟發,它允許非同步函式返回錯誤和結果。

  簡單地說,我們可以像這樣使用非同步函式:

[err, user] = await to(UserModel.findById(1));

  我個人不喜歡這種方法,因為它將Go語言的風格帶入到了JavaScript中,感覺不自然。但在某些情況下,這可能相當有用。

  使用.catch

  我們要介紹的最後一種方法是繼續使用.catch()。

  回想一下await的功能:它將等待promise完成工作。另外,promise.catch()也會返回一個promise!所以我們可以這樣進行錯誤處理:

// books === undefined if error happens,
// since nothing returned in the catch statement
let books = await bookModel.fetchAll()
  .catch((error) => { console.log(error); });

  這種方法有兩個小問題:

  • 它是promise和非同步函式的混合體。你仍然需要了解promise的工作原理才能看懂這段程式碼。
  • 錯誤處理出現在普通程式碼邏輯之前,這樣不直觀。

 結論

  ES7引入的async/await關鍵字絕對是對JavaScript非同步程式設計的重大改進。它讓程式碼更易於閱讀和除錯。然而,要正確使用它們,人們必須瞭解promise。它們不過是語法糖,本質上仍然是promise。

  英文原文:https://hackernoon.com/javascript-async-await-the-good-part-pitfalls-and-how-to-use-9b759ca21cda

相關文章