2022年2月26日 非同期処理:コールバック/Promise/Async - await式はAsync Functionの直下でのみ利用可能 (JavaScript Primer)
await式はAsync Functionの直下でのみ利用可能
今回はAsync Functionとawait式の特徴についてもう少し深ぼる内容でした。
関数宣言でawait式を使用した場合
// asyncではない関数では`await`式は利用できない function main(){ // SyntaxError: await is only valid in async functions await Promise.resolve(); }
Async Function内でawait式を使って処理を待っている間も、関数の外側では通常どおり処理が進みます。 次のコードを実行してみると、Async Function内でawaitしても、Async Function外の処理は停止していないことがわかります。
async function asyncMain() { // 中でawaitしても、Async Functionの外側の処理まで止まるわけではない await new Promise((resolve) => { setTimeout(resolve, 16); }); } console.log("1. asyncMain関数を呼び出します"); // Async Functionは外から見れば単なるPromiseを返す関数 asyncMain().then(() => { console.log("3. asyncMain関数が完了しました"); }); // Async Functionの外側の処理はそのまま進む console.log("2. asyncMain関数外では、次の行が同期的に呼び出される");
- Async Functionでawait式を使った場合、外側の処理にawaitが影響することはありません。
Async Function内でコールバック関数を使った場合
for文で繰り返し処理をしてきましたが、ここではArray#forEach
に変更しています。この場合に先述したAsync Functionの外側の処理にawaitが影響しない事で望む挙動が得られなくなってきます。
async function fetchResources(resources) { const results = []; // Syntax Errorとなる例 resources.forEach(function(resource) { // Async Functionではないスコープで`await`式を利用しているためSyntax Errorとなる const response = await dummyFetch(resource); results.push(response.body); }); return results; }
- Async Function内でforEachなどでコールバック関数を書く場合、コールバック関数に対してもasync宣言を行わないとawait式を使った場合にSyntaxErrorとなります。
- コールバック関数内のブロックのawaitはコールバック関数の外側を参照しないためです。
Async Function内のコールバック関数をAsync Functionにした場合
forEachメソッドのコールバック関数をAsync Functionにした場合の挙動をみていきます。
syntax errorにはなりませんが望んだ挙動は得られません。
以下、コード例です。問題点がわかるようにconsole.log
で出力する順番をみていきます。
function dummyFetch(path) { return new Promise((resolve, reject) => { setTimeout(() => { if (path.startsWith("/resource")) { resolve({ body: `Response body of ${path}` }); } else { reject(new Error("NOT FOUND")); } }, 1000 * Math.random()); }); } // リソースを順番に取得する async function fetchResources(resources) { const results = []; console.log("1. fetchResourcesを開始"); resources.forEach(async function(resource) { console.log(`2. ${resource}の取得開始`); const response = await dummyFetch(resource); // `dummyFetch`が完了するのは、`fetchResources`関数が返したPromiseが解決された後 console.log(`5. ${resource}の取得完了`); results.push(response.body); }); console.log("3. fetchResourcesを終了"); return results; } const resources = ["/resource/A", "/resource/B"]; // リソースを取得して出力する fetchResources(resources).then((results) => { console.log("4. fetchResourcesの結果を取得"); console.log(results); // => [] });
- fetchResources(resources)は非同期処理が行われています。
- 非同期処理内ではコールバック関数がAsync Functionでawait式が使われていますが外側には反映されません。
- awaitの処理を待っている間に非同期処理が進み、先に結果をreturnします。
- Fulfilled状態の[]が返り値となりsettledとしてthenメソッドが実行され、4つ目のconsole.log();とresultsが出力されます。
- resultsが出力された後にawaitの5つ目のconsole.log();が出力されます。
- forEachのコールバック関数をArrowFunctionに変えても同様の結果になります。
まとめ
この問題を解決する方法として、最初のfetchResources関数のように、コールバック関数を使わずにすむforループとawait式を組み合わせる方法があります。 また、fetchAllResources関数のように、複数の非同期処理を1つのPromiseにまとめることでループ中にawait式を使わないようにする方法があります。
- await式を利用する場合はasync宣言をしないとSyntaxErrorになります。
- await式は定義されたスコープのAsyncFunction外には影響しません。
- AsyncFunction内でコールバック関数を使う場合は注意しましょう。