䟋を䜿甚しお JavaScript の Async/Await を芋おみたしょう

この蚘事の著者は、JavaScript における Async/Await の䟋を調べおいたす。 党䜓ずしお、Async/Await は非同期コヌドを䜜成する䟿利な方法です。 この機胜が登堎する前は、そのようなコヌドはコヌルバックずプロミスを䜿甚しお蚘述されおいたした。 元の蚘事の著者は、さたざたな䟋を分析するこずで Async/Await の利点を明らかにしおいたす。

リマむンダヌ 「Habr」のすべおの読者が察象 - 「Habr」プロモヌション コヌドを䜿甚しおスキルボックス コヌスに登録するず 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 に倉換したす。 倉換が成功するず、ナヌザヌの電子メヌルにレタヌが送信されたす。 次に、むベントに関する情報が、日付を瀺すtransformations.log ファむルに入力されたす。

コヌドの最埌の郚分にあるコヌルバックの重耇ず倚数の }) に泚意する必芁がありたす。 それはコヌルバック地獄たたは砎滅のピラミッドず呌ばれたす。

この方法の欠点は明らかです。

  • このコヌドは読みにくいです。
  • ゚ラヌの凊理も難しく、コヌドの品質が䜎䞋するこずがよくありたす。

この問題を解決するために、Promise が JavaScript に远加されたした。 これにより、コヌルバックの深いネストを .then ずいう単語に眮き換えるこずができたす。

Promise の良い面は、コヌドを巊から右ではなく䞊から䞋に読みやすくするこずです。 ただし、Promise には問題もありたす。

  • .then を倧量に远加する必芁がありたす。
  • try/catch の代わりに、.catch を䜿甚しおすべおの゚ラヌが凊理されたす。
  • XNUMX ぀のルヌプ内で耇数の Promise を扱うこずは必ずしも䟿利であるずは限りたせん。堎合によっおは、コヌドが耇雑になりたす。

ここでは、最埌の点の意味を瀺す問題を瀺したす。

0 から 10 たでの䞀連の数倀をランダムな間隔 (0  n 秒) で出力する for ルヌプがあるずしたす。 Promise を䜿甚しお、数倀が 0 から 10 たで順番に出力されるようにこのルヌプを倉曎する必芁がありたす。぀たり、6 を出力するのに 2 秒かかり、XNUMX を出力するのに XNUMX 秒かかる堎合は、最初に XNUMX を出力し、その埌に出力する必芁がありたす。印刷のカりントダりンが始たりたす。

そしおもちろん、この問題を解決するために Async/Await や .sort は䜿甚したせん。 解決策の䟋は最埌にありたす。

非同期関数

ES2017 (ES8) に非同期関数が远加されたこずで、Promise を扱うタスクが簡玠化されたした。 async 関数は Promise の「䞊で」動䜜するこずに泚意しおください。 これらの関数は、質的に異なる抂念を衚すものではありたせん。 非同期関数は、Promise を䜿甚するコヌドの代替ずしお意図されおいたす。

Async/Await を䜿甚するず、非同期コヌドを䜿甚した䜜業を同期スタむルで敎理できたす。

したがっお、Promise を理解するず、Async/Await の原則を理解しやすくなりたす。

構文

通垞、これは async ず await の XNUMX ぀のキヌワヌドで構成されたす。 最初の単語は関数を非同期に倉換したす。 このような関数では 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');
  }
}

泚意 クラス コンストラクタヌずゲッタヌ/セッタヌは非同期にできないこずを芚えおおく䟡倀がありたす。

セマンティクスず実行ルヌル

非同期関数は基本的に暙準の 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 でラップされたす。

戻り倀がプリミティブの堎合、async 関数は倀を 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 を返したす。 この状況では、Promise.resolve の代わりに、゚ラヌを含む Promise.reject が返されたす。

非同期関数は、䜕が返されるかに関係なく、垞に Promise を出力したす。

非同期関数は await ごずに䞀時停止したす。

Await は匏に圱響したす。 したがっお、匏が Promise である堎合、async 関数は 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 = await 9; から倉換されたす。 const a = await Promise.resolve(9);。
  • Await を䜿甚した埌、関数の実行は、 a がその倀 (珟圚の状況では 9) を取埗するたで䞀時停止されたす。
  • lateAndGetRandom(1000) は、関数自䜓が完了するたで (1 秒埌) fn 関数の実行を䞀時停止したす。 これにより、fn 機胜が 1 秒間実質的に停止されたす。
  • replaceAndGetRandom(1000) は、resolve を介しおランダムな倀を返し、それが倉数 b に割り圓おられたす。
  • さお、倉数 c の堎合は倉数 a の堎合ず䌌おいたす。 その埌、すべおが䞀瞬停止したすが、delayAndGetRandom(1000) は必芁ないため、䜕も返したせん。
  • その結果、倀は匏 a + b * c を䜿甚しお蚈算されたす。 結果は Promise.resolve を䜿甚しお Promise にラップされ、関数によっお返されたす。

これらの䞀時停止は ES6 のゞェネレヌタヌを思い出させるかもしれたせんが、それには䜕かがありたす あなたの理由.

問題の解決

さお、䞊で述べた問題の解決策を芋おみたしょう。

finishMyTask 関数は、Await を䜿甚しお、queryDatabase、sendEmail、logTaskInFile などの操䜜の結果を埅ちたす。 この゜リュヌションを Promise を䜿甚した゜リュヌションず比范するず、類䌌点が明らかになりたす。 ただし、Async/Await バヌゞョンでは、すべおの耇雑な構文が倧幅に簡玠化されおいたす。 この堎合、.then/.catch のような倚数のコヌルバックやチェヌンはありたせん。

ここでは数倀を出力する゜リュヌションを瀺したす。オプションは XNUMX ぀ありたす。

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

ОбрабПтка ПсОбПк

未凊理の゚ラヌは、拒吊された Promise にラップされたす。 ただし、非同期関数は 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';
  }
}

この䟋では、foo から canRejectOrReturn が返されるこずに泚意しおください。 この堎合、Foo は完党な数倀で終了するか、゚ラヌ (「申し蚳ありたせん、数倀が倧きすぎたす」) を返したす。 catch ブロックは決しお実行されたせん。

問題は、foo が canRejectOrReturn から枡された Promise を返すこずです。 したがっお、foo に察する解決策は canRejectOrReturn に察する解決策になりたす。 この堎合、コヌドは XNUMX 行のみで構成されたす。

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

await ず return を䞀緒に䜿甚するずどうなるかは次のずおりです。

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

䞊蚘のコヌドでは、foo は完党数ず゚ラヌの䞡方をキャッチしお正垞に終了したす。 ここで拒吊はありたせん。 ただし、foo は unknown ではなく、canRejectOrReturn で戻りたす。 return await canRejectOrReturn() 行を削陀しお、これを確認したしょう。

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


よくある間違いず萜ずし穎

堎合によっおは、Async/Await を䜿甚するず゚ラヌが発生する可胜性がありたす。

忘れられたたた埅っおいたす

これは非垞に頻繁に発生したす。Promise の前に await キヌワヌドが忘れられおいたす。

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

ご芧のずおり、コヌドには await や return がありたせん。 したがっお、foo は垞に 1 秒の遅延なく undefine で終了したす。 しかし、玄束は必ず果たされたす。 ゚ラヌたたは拒吊がスロヌされた堎合は、UnhandledPromiseRejectionWarning が呌び出されたす。

コヌルバックの非同期関数

非同期関数は、.map たたは .filter でコヌルバックずしおよく䜿甚されたす。 䟋ずしおは、GitHub 䞊で開いおいるリポゞトリの数を返す fetchPublicReposCount(username) 関数がありたす。 メトリクスが必芁なナヌザヌが XNUMX 人いるずしたす。 このタスクのコヌドは次のずおりです。

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 配列に远加されたす。 このコヌドの問題は、最初のナヌザヌのデヌタがサヌバヌから到着するたで、埌続のナヌザヌはすべおスタンバむ モヌドになるこずです。 したがっお、䞀床に凊理されるナヌザヌは XNUMX 人だけです。

たずえば、300 人のナヌザヌの凊理に玄 XNUMX ミリ秒かかる堎合、すべおのナヌザヌではすでに XNUMX 秒になっおおり、費やされる時間はナヌザヌの数に盎線的に䟝存したす。 ただし、リポゞトリ数の取埗は盞互に䟝存しないため、凊理を䞊列化するこずができたす。 これには、.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 を返したす。 埌者は、配列内のすべおの Promise が完了した埌、たたは最初の拒吊時に完了したす。 すべおが同時に開始しない堎合がありたす。確実に同時開始するには、p-map を䜿甚したす。

たずめ

開発にずっお非同期関数はたすたす重芁になっおいたす。 そうですね、非同期関数を適応的に䜿甚するには、䜿甚する䟡倀がありたす 非同期反埩子。 JavaScript 開発者は、この点に粟通しおいる必芁がありたす。

スキルボックスは次のこずを掚奚したす。

出所 habr.com

コメントを远加したす