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

非同期関数を使用すると、同期コードのようにプロミスベースのコードを書くことができます。

非同期関数は、Chrome、Edge、Firefox、Safari でデフォルトで有効になっています。正直なところ、非同期関数は素晴らしいものです。これらの関数を使用すると、メインスレッドをブロックすることなく、同期コードのように Promise ベースのコードを書くことができます。これにより、非同期コードの「巧妙さ」が低減され、読みやすくなります。

非同期関数は次のように機能します。

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

関数定義の前に async キーワードを使用すると、関数内で await を使用できます。Promise を await すると、Promise が解決されるまで関数は非ブロッキングで一時停止されます。Promise が解決すると、値が返されます。Promise が拒否された場合、拒否された値がスローされます。

ブラウザ サポート

Browser Support

  • Chrome: 55.
  • Edge: 15.
  • Firefox: 52.
  • Safari: 10.1.

Source

例: 取得のロギング

URL を取得してレスポンスをテキストとしてログに記録するとします。プロミスを使用すると、次のように表示されます。

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')rejectsする 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);     });   }); } 

私、Jake 「約束の使い手」Archibald をチェックしてください。非同期ループを設定するために、自身内で processResult() を呼び出していることがわかります。書いているうちに、とても賢いと感じました。しかし、ほとんどの「スマート」なコードと同様に、90 年代の魔法の絵のように、何をしているのかを理解するには、長い時間見つめ続ける必要があります。

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

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 を取得し、できるだけ早く正しい順序でログに記録したいとします。

深呼吸 - プロミスを使用すると、次のように表示されます。

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());   } }
はるかにきれいに見えますが、最初のフェッチが完全に読み取られるまで 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);   } }
この例では、URL は並列で取得および読み取られますが、「スマート」な reduce ビットは、標準の読みやすい for ループに置き換えられています。

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

ジェネレータをサポートするブラウザ(すべての主要ブラウザの最新バージョンを含む)をターゲットとしている場合は、非同期関数をポリフィルできます。

Babel が自動的に行います。Babel REPL を使用した例

トランスパイル アプローチをおすすめします。これは、ターゲット ブラウザが非同期関数をサポートしたら、トランスパイルをオフにできるためです。トランスパイラーを本当に使用したくない場合は、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 年に非同期関数に非常に興味を持ち、ブラウザで実際に実装されるのを楽しみにしていましたが、ついに実現しました。わーい!