非同期関数を使用すると、同期コードのようにプロミスベースのコードを書くことができます。
非同期関数は、Chrome、Edge、Firefox、Safari でデフォルトで有効になっています。正直なところ、非同期関数は素晴らしいものです。これらの関数を使用すると、メインスレッドをブロックすることなく、同期コードのように Promise ベースのコードを書くことができます。これにより、非同期コードの「巧妙さ」が低減され、読みやすくなります。
非同期関数は次のように機能します。
async function myFirstAsyncFunction() { try { const fulfilledValue = await promise; } catch (rejectedValue) { // … } }
関数定義の前に async
キーワードを使用すると、関数内で await
を使用できます。Promise を await
すると、Promise が解決されるまで関数は非ブロッキングで一時停止されます。Promise が解決すると、値が返されます。Promise が拒否された場合、拒否された値がスローされます。
ブラウザ サポート
例: 取得のロギング
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()); } }
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); } }
reduce
ビットは、標準の読みやすい for ループに置き換えられています。 ブラウザ サポートの回避策: ジェネレータ
ジェネレータをサポートするブラウザ(すべての主要ブラウザの最新バージョンを含む)をターゲットとしている場合は、非同期関数をポリフィルできます。
Babel が自動的に行います。Babel REPL を使用した例
- 変換されたコードがどれほど類似しているかに注目してください。この変換は Babel の es2017 プリセットの一部です。
トランスパイル アプローチをおすすめします。これは、ターゲット ブラウザが非同期関数をサポートしたら、トランスパイルをオフにできるためです。トランスパイラーを本当に使用したくない場合は、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 年に非同期関数に非常に興味を持ち、ブラウザで実際に実装されるのを楽しみにしていましたが、ついに実現しました。わーい!