【JavaScript】非同期処理について(async/await)
はじめに
こんにちは。SHIFTのDevOps推進部に所属しているozakiです。
これまでに、async/awaitを理解するという目標に向かって
と学びを整理してきました。
そして今回はasync/awaitについて学んだ事をまとめてみようと思います。
async関数
asyncとは
async関数とは、非同期処理を行う関数を定義する構文のことです。
async関数は必ずPromiseオブジェクトを返す関数を定義します。そのため、async関数を理解しようとするためにはPromiseへの理解が欠かせません。
asyncの使い方
async関数は、関数の前にasyncを付ける事で定義することができます。
例1はPromiseを、例2はasyncを用いて、同じ処理を書いたものです。
// 例1
function examplePromise() {
return new Promise.resolve('ans');
}
examplePromise().then((value)=> {
console.log(value);
});
// 例2
async function examplePromise() {
return 'ans';
}
examplePromise().then((value)=> {
console.log(value);
});
関数の前にasyncを付けることで、Promise.resolve()のようにPromiseオブジェクトを返してくれます。 もちろん、このPromiseオブジェクトにはfulfilledやrejectedのような状態があります。
もしこのPromiseオブジェクトの状態について知りたければ、こちらの記事を読んでみてください。
await式
awaitとは
awaitとは、async関数の直下で使うことのできる式です。
awaitの使い方
awaitはPromiseを返す関数呼び出しの前に付けることで、そのPromiseオブジェクトが完了する(settled)までその文で処理を待ちます。そして、Promiseオブジェクトが完了すると、次の文から処理を再開します。
またawaitはPromiseを受け取り、戻り値やスローされた例外に変換します。
// 例3
async function asyncMain() {
const value = await Promise.resolve(10);
console.log('この行以降は非同期処理が完了後に実行される');
console.log(value); // => 10
}
例3のようにawaitが付けられたPromiseオブジェクトがfulfilledの場合、valueの値はPromiseオブジェクトを満たした値(10)になります。
一方でPromiseオブジェクトがrejectedの場合、例4のようにawaitはエラーをスローします。
// 例4
async function asyncMain() {
const value = await Promise.reject(new Error('エラーメッセージ'));
}
asyncMain().catch(error => {
console.log(error.message); // => 'エラーメッセージ'
});
非同期処理は通常のtry...catch構文の使い方ではエラーがキャッチできず、非常に見づらい書き方をせざるを得ませんでした。
await式を使うことで同期処理と同じように扱うことができるようになります。
コードの可読性も損なわれず、同期処理と同じような分かりやすい形で表現できるのがasync/awaitの大きな利点です。
Promiseチェーンをawaitで表現
async/awaitの利点として、非同期処理を同期処理のような見た目でかける事を挙げました。
ここではPromiseチェーンとasync/awaitを使った場合を比較して見てみましょう。
// 例5
function determinePath(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());
});
}
// Promiseチェーンの場合-----------------------------------------------
function main() {
const results = [];
return determinePath('/resource/A').then(response => {
results.push(response.body);
return determinePath('/resource/B');
}).then(response => {
results.push(response.body);
return results;
});
}
// --------------------------------------------------------------------
// async/awaitの場合---------------------------------------------------
async functionmain main() {
const results = [];
const responseA = await determinePath('/resource/A');
results.push(responseA.body);
const responseB = await determinePath('/resource/B');
results.push(responseB.body);
return results;
}
// -------------------------------------------------------------------
main().then((results) => {
console.log(results); // => ['Response body of /resource/A', 'Response body of /resource/B']
});
両者を比較すると、async/awaitはネストがなく、シンプルに書くことができています。
非同期処理における反復
Promiseではpromise.all()を使うことで、複数の非同期処理を1つの配列としてまとめて処理することが可能でした。
async/awaitの場合は非同期処理を配列にまとめることで、for文を使い処理をループさせることができます。
以下の例6がfor文を使ったコードです。
// 例6
function determinePath(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 main(resources) {
const results = [];
for (const resource of resources) {
const response = await determinePath(resource);
results.push(response.body);
}
return results;
}
const resources = [
'/resource/A',
'/resource/B'
];
main(resources).then((results) => {
console.log(results); // => ['Response body of /resource/A', 'Response body of /resource/B']
});
非同期処理を同期処理のようなに書くことができるのは理解がしやすく良いことだと思います。
しかし、なぜそもそも非同期処理が使われているのでしょうか。それは非同期処理は、次々に処理を進めることで無駄な待ち時間を減らすことができるからでした。
もし例6の'/resource/A'の処理時間がとても長かったとしたら、'/resource/B'はそれを待つことになるので、無駄な待ち時間が発生します。つまり、非同期処理のメリットを活用できていないのです。
そんなときはどうすればよいのでしょうか。次はその解決法の一つを見ていきます。
Promise.all()の活用
例7では、Promise.allメソッドとAsync Functionを併用して、先ほどの問題を解決しています。Promise.allメソッドの返すPromiseインスタンスをawaitすることで、非同期処理の結果を配列としてまとめて取得できます。
await式が評価するのはPromiseインスタンスであるため、await式もPromise.allメソッドと組み合わせて利用できるのです。
// 例7
function determinePath(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 main(resources) {
const promises = resources.map((resource) => {
return determinePath(resource);
});
const responses = await Promise.all(promises);
return responses.map((response) => {
return response.body;
});
}
const resources = [
'/resource/A',
'/resource/B'
];
main(resources).then((results) => {
console.log(results); // => ['Response body of /resource/A', 'Response body of /resource/B']
});
async/awaitはPromiseを分かりやすく記述できるように、改良されたものだと思います。しかし使う場面によっては、メリットを生かせないこともあるようです。
おわりに
最後に、await式を使うにあたっての注意点です。
それはawait式はAsync Functionの直下でのみ利用可能ということです。
もしasyncのない関数でawaitを使ってしまうと、SyntaxErrorとなります。
ネストを深くしてしまうと、asyncを付け忘れてしまい意図した動きにならないなんてこともありそうですね。
また場面によっては、Promiseのメソッドを使うことも視野に入れたほうが良いことも知りました。
これまで、callbackからPromise, async/awaitと非同期関数について学んできました。
これからは非同期関数をきちんと使いこなせるよう、さらに多くのコードに触れていきたいと思います。
参考文献
JavaScript Primer
https://jsprimer.netJavaScript Promiseの本
https://azu.github.io/promises-book/#chapter5-async-function「JavaScript 第7版」David Flanagan 著、村上 列 訳
《お問合せはお気軽に》
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
PHOTO:UnsplashのSafar Safarov