非同期関数を使用すると、Promise ベースのコードを同期関数のように記述できます。
非同期関数は、Chrome、Edge、Firefox、Safari でデフォルトで有効になっていますが、実に驚くべき点です。これにより、メインスレッドをブロックせずに、Promise ベースのコードを同期コードのように記述できます。これにより、非同期コードがあまり「巧妙」ではなく、読みやすくなります。
非同期関数は次のように機能します。
async function myFirstAsyncFunction() {
try {
const fulfilledValue = await promise;
} catch (rejectedValue) {
// …
}
}
関数定義の前に async
キーワードを使用すると、関数内で await
を使用できます。Promise を await
すると、Promise が解決するまで関数はブロック以外の方法で一時停止されます。Promise が解決すると、値が返されます。Promise が拒否された場合、拒否された値がスローされます。
ブラウザ サポート
例: 取得のロギング
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);
});
});
}
見てください、ジェイク、「約束の使い手」アーチボルド。非同期ループを設定するために、内部で 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 を取得して、できるだけ早く正しい順序でログに記録するとします。
深呼吸 - 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()); } }
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); } }
ブラウザ サポートの回避策: 生成ツール
ジェネレータ(すべてのメジャー ブラウザの最新バージョンを含む)をサポートするブラウザをターゲットにしている場合は、ポリフィル非同期関数のような方法を使用できます。
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 年に非同期関数に大きな期待を寄せていました。こうした非同期関数がブラウザにも実装されたのは、とても嬉しいことです。おっと!