非同期関数: Promise を使いやすくする

非同期関数を使用すると、Promise ベースのコードをあたかも同期であるかのように記述できます。

Chrome、Edge、Firefox、Safari では非同期関数がデフォルトで有効になっています。 率直に言って 素晴らしいことですこれを使用すると、Promise ベースのコードを メインスレッドをブロックせずに同期されていた場合。 あまり「巧妙」ではない非同期コード見やすくなるからです

非同期関数は次のように動作します。

async function myFirstAsyncFunction() {
  try {
    const fulfilledValue = await promise;
  } catch (rejectedValue) {
    // …
  }
}

関数定義の前に async キーワードを使用すると、 関数内の await。Promise に対して await を実行すると、関数は一時停止します。 Promise が解決するまで、ブロッキングせずに実行します。Promise が履行されると、 価値を取り戻すことができますPromise が拒否された場合は、拒否された値がスローされます。

ブラウザ サポート

対応ブラウザ

  • Chrome: 55。 <ph type="x-smartling-placeholder">
  • Edge: 15。 <ph type="x-smartling-placeholder">
  • Firefox: 52。 <ph type="x-smartling-placeholder">
  • Safari: 10.1。 <ph type="x-smartling-placeholder">

ソース

例: 取得をログに記録する

URL を取得し、レスポンスをテキストとして記録するとします。画面サンプル Promise を使用します。

function logFetch(url) {
  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      console.log(text);
    })
    .catch((err) => {
      console.error('fetch failed', err);
    });
}

非同期関数を使用した場合も同じです。

async function logFetch(url) {
  try {
    const response = await fetch(url);
    console.log(await response.text());
  } catch (err) {
    console.log('fetch failed', err);
  }
}

行数は同じですが、コールバックはすべてなくなっています。これにより、 特に Promise に詳しくない人にとっては読みやすくなります。

非同期戻り値

非同期関数は、await を使用するかどうかにかかわらず、常に Promise を返します。 Promise は、非同期関数が返したものを使用して解決するか、 非同期関数がスローするもの次のようになります。

// wait ms milliseconds
function wait(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function hello() {
  await wait(500);
  return 'world';
}

hello() を呼び出すと、"world"満たされる Promise が返されます。

async function foo() {
  await wait(500);
  throw Error('bar');
}

foo() を呼び出すと、Error('bar') で拒否する Promise が返されます。

例: レスポンスのストリーミング

複雑な例の場合、非同期関数のメリットはさらに大きくなります。たとえば チャンクのログアウト中にレスポンスをストリーミングし、最終的なサイズを返します。

Promise の場合は次のようになります。

function getResponseSize(url) {
  return fetch(url).then((response) => {
    const reader = response.body.getReader();
    let total = 0;

    return reader.read().then(function processResult(result) {
      if (result.done) return total;

      const value = result.value;
      total += value.length;
      console.log('Received chunk', value);

      return reader.read().then(processResult);
    });
  });
}

見てください、ジェイク「約束の使い手」ですArchibald。通話状況を確認 processResult() を内部に使用して非同期ループを設定しますか?心に響く作風 とても頭がいいと感じます。しかし、ほとんどの「スマート」コードを見落とさないように、 何が起こっているのかを理解できません あります

非同期関数でもう一度試してみましょう。

async function getResponseSize(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  let result = await reader.read();
  let total = 0;

  while (!result.done) {
    const value = result.value;
    total += value.length;
    console.log('Received chunk', value);
    // get the next result
    result = await reader.read();
  }

  return total;
}

「スマート」なくなります。非同期ループで、私を自分に甘く感じたのは、 代わりに 信頼できる退屈な while ループに置き換わります。はるかに良くなりました。将来的には、 非同期イテレータ これは、 while ループを for-of ループに置き換えて、さらにすっきりさせます。

その他の非同期関数の構文

async function() {} についてはすでに説明しましたが、async キーワードは次のように設定できます。 次の関数構文で使用できます。

アロー関数

// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
  const response = await fetch(url);
  return response.json();
});

オブジェクトのメソッド

const storage = {
  async getAvatar(name) {
    const cache = await caches.open('avatars');
    return cache.match(`/avatars/${name}.jpg`);
  }
};

storage.getAvatar('jaffathecake').then();

クラス メソッド

class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jaffathecake').then();

気をつけて!順番通りにしすぎない

同期しているように見えるコードを記述していますが、 並列して実行する機会が得られます

async function series() {
  await wait(500); // Wait 500ms…
  await wait(500); // …then wait another 500ms.
  return 'done!';
}

上記の処理が完了するまでに 1, 000 ミリ秒かかりますが、次の処理は完了です。

async function parallel() {
  const wait1 = wait(500); // Start a 500ms timer asynchronously…
  const wait2 = wait(500); // …meaning this timer happens in parallel.
  await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
  return 'done!';
}

両方の待機が同時に発生するため、上記が完了するまでに 500 ミリ秒かかります。 実用的な例を見てみましょう。

例: フェッチを順番に出力する

一連の URL をフェッチして、これらの URL をできるだけ早く 正しい順序になります。

深呼吸 - Promise では次のようになります。

function markHandled(promise) {
  promise.catch(() => {});
  return promise;
}

function logInOrder(urls) {
  // fetch all the URLs
  const textPromises = urls.map((url) => {
    return markHandled(fetch(url).then((response) => response.text()));
  });

  // log them in order
  return textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise).then((text) => console.log(text));
  }, Promise.resolve());
}

そうです。reduce を使用して一連の Promise を連結しています。たぶん スマート。ただし、これは非常にスマートなコーディングなので、使わない方が得策です。

ただし、上記を非同期関数に変換すると、 逐次的すぎる:

非推奨 - 順次的すぎる
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
<ph type="x-smartling-placeholder"></ph> 見た目はかなりすっきりしていますが、2 回目の取得は最初の取得が完了するまで開始されません。 表示されます。これは、次の Promise の例よりもはるかに低速です。 並列してフェッチを実行します。幸い、理想的な中間点があります。
推奨 - 優れた並列性
function markHandled(...promises) {
  Promise.allSettled(promises);
}

async function logInOrder(urls) {
  // fetch all the URLs in parallel
  const textPromises = urls.map(async (url) => {
    const response = await fetch(url);
    return response.text();
  });

  markHandled(...textPromises);

  // log them in sequence
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}
<ph type="x-smartling-placeholder"></ph> この例では、URL の取得と読み取りが並行して行われますが、「スマート」な reduce ビットが、退屈で読みやすい標準の for ループに置き換えられます。
で確認できます。

ブラウザ サポートの回避策: ジェネレータ

ジェネレータ( すべての主要なブラウザの最新バージョンに )を使用すると、非同期関数をポリフィルできます。

Babel が代わりに行いますが、 Babel REPL を使用した例をご覧ください

で確認できます。

トランスパイル方法をおすすめします。 対象のブラウザは非同期関数をサポートしていますが、 Transpiler を使用すると、 Babel のポリフィル ご自身で使用してみてください。従来の方法:

async function slowEcho(val) {
  await wait(1000);
  return val;
}

ポリフィルを含める 次のように記述します。

const slowEcho = createAsyncFunction(function* (val) {
  yield wait(1000);
  return val;
});

なお、ジェネレータ(function*)を createAsyncFunction に渡す必要があることに留意してください。 await の代わりに yield を使用します。それ以外は同じように動作します。

回避策: リジェネレータ

古いブラウザをターゲットにしている場合、Babel では ジェネレータのトランスパイルも可能です IE8 まで非同期関数を使用できます。これを行うには、 Babel の es2017 プリセット es2015 プリセットを指定します。

出力があまり見栄えが良くないため、 減ります。

すべてを非同期にします。

すべてのブラウザで非同期関数を導入したら、すべてのブラウザで Promise を返す関数です。コードを整理できるだけでなく 関数が常に Promise を返すことを確認します。

先日、非同期関数にとても興味を持ちました。 2014 年、 実際にブラウザに表示されるのは素晴らしいことです。やりました!