讓我們使用範例來了解 JavaScript 中的 Async/Await

本文作者研究了 JavaScript 中 Async/Await 的範例。 總的來說,Async/Await 是編寫非同步程式碼的一種便捷方式。 在此功能出現之前,此類程式碼是使用回調和 Promise 編寫的。 原文章作者透過分析各種例子揭示了Async/Await的優點。

提醒: 對於“Habr”的所有讀者 - 使用“Habr”促銷代碼註冊任何 Skillbox 課程可享受 10 盧布的折扣。

技能箱推薦: 教育在線課程 《Java 開發人員》.

回調

回調是一種無限期延遲呼叫的函數。 以前,回調用於那些無法立即取得結果的程式碼區域。

以下是 Node.js 中非同步讀取檔案的範例:

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

當您需要同時執行多個非同步操作時,就會出現問題。 讓我們想像一下這樣的場景:向 Arfat 用戶資料庫發出請求,您需要讀取其 profile_img_url 欄位並從 someserver.com 伺服器下載映像。
下載後,我們將圖像轉換為其他格式,例如從 PNG 轉換為 JPEG。 如果轉換成功,則會向使用者的電子郵件發送一封信。 接下來,有關事件的資訊將輸入到 conversions.log 檔案中,並指示日期。

值得注意的是程式碼最後部分的回調重疊和大量})。 它被稱為回調地獄或末日金字塔。

這種方法的缺點是顯而易見的:

  • 這段程式碼很難閱讀。
  • 錯誤處理也很困難,這通常會導致程式碼品質較差。

為了解決這個問題,JavaScript 中加入了 Promise。 它們允許您用 .then 一詞替換回調的深層嵌套。

Promise 的積極方面是它們使程式碼從上到下而不是從左到右更具可讀性。 然而,Promise 也有其問題:

  • 你需要加很多.then。
  • .catch 取代 try/catch 來處理所有錯誤。
  • 在一個循環中使用多個 Promise 並不總是很方便;在某些情況下,它們會使程式碼變得複雜。

這裡有一個問題將顯示最後一點的含義。

假設我們有一個 for 循環,它以隨機間隔(0-n 秒)列印從 10 到 0 的數字序列。 使用 Promise,您需要更改此循環,以便按從 0 到 10 的順序列印數字。因此,如果列印 6 需要 2 秒,列印 XNUMX 需要 XNUMX 秒,則應該先列印 XNUMX,然後再列印列印該檔案的倒數計時即將開始。

當然,我們不使用 Async/Await 或 .sort 來解決這個問題。 最後是一個範例解決方案。

非同步函數

ES2017 (ES8) 中新增的非同步函數簡化了使用 Promise 的任務。 我注意到非同步函數在承諾的“之上”工作。 這些功能並不代表本質上不同的概念。 非同步函數旨在作為使用 Promise 的程式碼的替代方案。

Async/Await 使得以同步方式組織非同步方式組織非同步程式碼工作成為可能。

因此,了解 Promise 可以更輕鬆地理解 Async/Await 的原理。

句法

通常它由兩個關鍵字組成: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 插入在函數宣告的最開始處,如果是箭頭函數,則插入在「=」號和括號之間。

這些函數可以作為方法放置在物件中,也可以在類別聲明中使用。

// 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');
  }
}

注意! 值得記住的是,類別建構子和 getter/setter 不能是異步的。

語意和執行規則

非同步函數與標準 JS 函數基本相似,但也有例外。

因此,非同步函數總是傳回 Promise:

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

具體來說,fn 傳回字串 hello。 好吧,由於這是一個非同步函數,因此使用建構函數將字串值包裝在 Promise 中。

這是沒有非同步的替代設計:

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

在這種情況下,promise 是「手動」返回的。 非同步函數總是包含在新的 Promise 中。

如果傳回值是原語,則非同步函數會透過將其包裝在 Promise 中來傳回該值。 如果傳回值是 Promise 對象,則其解析結果會在新的 Promise 中傳回。

const p = Promise.resolve('hello')
p instanceof Promise;
// true
 
Promise.resolve(p) === p;
// true
 

但是如果非同步函數內部出現錯誤會發生什麼事?

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

如果沒有被處理, foo() 將傳回一個帶有拒絕的承諾。 在這種情況下,將傳回包含錯誤的 Promise.reject 而不是 Promise.resolve。

無論返回什麼,非同步函數總是輸出一個承諾。

非同步函數在每次等待時暫停。

Await 影響表達式。 因此,如果表達式是一個 Promise,則非同步函數將被掛起,直到 Promise 得到履行。 如果表達式不是 Promise,則透過 Promise.resolve 將其轉換為 Promise,然後完成。

// 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 = wait 9; 轉換而來。 在 const a = wait Promise.resolve(9); 中。
  • 使用 Await 後,函數執行將暫停,直到 a 取得其值(在目前情況下為 9)。
  • delayAndGetRandom(1000) 暫停 fn 函數的執行,直到它自行完成(1 秒後)。 這實際上使 fn 函數停止 1 秒鐘。
  • 透過resolve的delayAndGetRandom(1000)傳回一個隨機值,然後將其指派給變數b。
  • 那麼,變數 c 的情況與變數 a 的情況類似。 之後,一切都會停止一秒鐘,但現在delayAndGetRandom(1000)不會返回任何內容,因為它不是必需的。
  • 結果,使用公式 a + b * c 計算這些值。 結果使用 Promise.resolve 包裝在 Promise 中並由函數傳回。

這些停頓可能會讓人想起 ES6 中的生成器,但它確實有一些東西 你的理由.

解決問題

好了,現在我們來看看上面提到的問題的解決方案。

finishMyTask 函數使用 Await 來等待 queryDatabase、sendEmail、logTaskInFile 等操作的結果。 如果將此解決方案與使用 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 function printNumbersUsingAsync() {
  for (let i = 0; i < 10; i++) {
    await wait(i, Math.random() * 1000);
    console.log(i);
  }
}

錯誤處理

未處理的錯誤包含在被拒絕的承諾中。 但是,非同步函數可以使用 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() 是一個非同步函數,它要麼成功(「完美數字」),要麼失敗並出現錯誤(「抱歉,數字太大」)。

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

由於上面的範例期望 canRejectOrReturn 執行,因此它本身的失敗將導致 catch 區塊的執行。 結果,函數 foo 將以未定義(當 try 區塊中沒有傳回任何內容時)或捕獲錯誤結束。 因此,函數不會失敗,因為 try/catch 將處理函數 foo 本身。

這是另一個例子:

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

值得注意的是,在範例中,canRejectOrReturn 是從 foo 傳回的。 在這種情況下,Foo 要么以完美數字終止,要么返回錯誤(“抱歉,數字太大”)。 catch 區塊永遠不會被執行。

問題是 foo 回傳從 canRejectOrReturn 傳遞的承諾。 所以 foo 的解就變成了 canRejectOrReturn 的解。 在這種情況下,程式碼將僅包含兩行:

try {
    const promise = canRejectOrReturn();
    return promise;
}

如果同時使用 wait 和 return ,會發生以下情況:

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

在上面的程式碼中, foo 將成功退出,並捕獲一個完全數和一個錯誤。 這裡不會有人拒絕。 但 foo 將以 canRejectOrReturn 返回,而不是 undefined。 讓我們透過刪除 return wait canRejectOrReturn() 行來確保這一點:

try {
    const value = await canRejectOrReturn();
    return value;
}
// …

常見錯誤和陷阱

在某些情況下,使用 Async/Await 可能會導致錯誤。

被遺忘的等待

這種情況經常發生——在承諾之前忘記了await關鍵字:

async function foo() {
  try {
    canRejectOrReturn();
  } catch (e) {
    return 'caught';
  }
}

正如你所看到的,程式碼中沒有await或return。 因此 foo 總是以 undefined 退出,沒有 1 秒的延遲。 但承諾一定會實現。 如果它拋出錯誤或拒絕,則將呼叫 UnhandledPromiseRejectionWarning。

回調中的非同步函數

非同步函數經常在 .map 或 .filter 中用作回調。 一個例子是 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;
});

值得關注 .map 回呼中的 Await。 這裡 counts 是一個 Promise 數組,.map 是每個指定使用者的匿名回呼。

過度一致地使用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 陣列中。 該程式碼的問題在於,在第一個用戶的資料從伺服器到達之前,所有後續用戶都將處於待機模式。 因此,一次僅處理一個使用者。

例如,如果處理一個使用者大約需要 300 毫秒,那麼對於所有使用者來說已經是一秒了;所花費的時間線性取決於使用者數量。 但由於獲取repo的數量並不依賴彼此,所以這些過程可以並行化。 這需要使用 .map 和 Promise.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。 後者在數組中的所有承諾完成後或在第一次拒絕時完成。 可能會發生它們不是同時啟動的情況——為了確保同時啟動,可以使用p-map。

結論

非同步函數對於開發變得越來越重要。 那麼,為了自適應使用非同步函數,您應該使用 非同步迭代器。 JavaScript 開發人員應該精通這一點。

技能箱推薦:

來源: www.habr.com

添加評論