async、await の使い方
JavaScript で非同期処理を作成する際に使用するのが async と await です。
ですが、この2つをちゃんと理解するのはかなり難しい気がします。
そこで、なんとか自分なりにわかりやすくまとめてみます。
誰かのお役に立てれば幸いです。
Promise の復習
async、await を語る前に、まずは Promise の復習から始めなければなりません。
Promise のことならバッチリ知ってるぜ!って方は、読み飛ばしてもらって構いません。
Promise とは?
あなたは何か処理に時間のかかる重たい処理を実装しているとします。 その処理をそのまま実行してしまうと、実行している間にユーザーの操作を待たせてしまうことになるので、 ユーザーの操作とは関係のない所でこっそりと裏で処理したいと考えたとします。 ユーザーの操作と関係ある所で堂々と処理することを「同期処理」と言い、 ユーザーの操作とは関係のない所でこっそりと裏で処理をすることを「非同期処理」と言います。
同期処理を実装するには特別なことは何もありません。 普通にコードを書くとそれは同期処理になります。 非同期処理を実装するには Promise という特別なオブジェクトを使用します。 Promise を使用するにはいくつかのルール(その名の通り約束)があります。 とはいえ、全部説明していてもあれなので、必要な点だけ説明します。
- Promise オブジェクトは「待機」「成功」「失敗」の3つの状態を持つ
- 処理が「成功」した時は
resolve()を呼ぶ必要がある - 処理が「失敗」した時は
reject()を呼ぶ必要がある - 処理が「成功」した時の処理は、
.then()で登録する(resolve()が呼ばれると実行される) - 処理が「失敗」した時の処理は、
.catch()で登録する(reject()が呼ばれると実行される) - 「成功」「失敗」にかかわらず行う処理は、
.finally()で登録する - Promise を使用した関数の戻り値は Promise にする必要がある
new Promise(function(resolve, reject) {
resolve();
}).then(function() {
console.log('then()');
}).catch(function() {
console.log('catch()');
}).finally(function() {
console.log('finally()');
});
then() finally()
実行されるタイミング
Promise オブジェクトのコンストラクタで登録された処理は同期処理になりますが、
.then()、.catch()、.finally() で登録された処理は非同期処理になります。
よく、コンストラクタで登録された処理が非同期になると誤解されている方がいますが、そうではないので注意してください。
ネットで見かけるサンプルでは、おそらく setTimeout() が使用されているはずです。
setTimeout() の中の部分は確かに非同期処理になりますが、その外の部分は同期処理です。
// この部分は同期処理 console.log('-- 同期処理開始 --'); // この部分は同期処理 new Promise(function(resolve, reject) { // この部分は同期処理 console.log('Promise() start'); // この部分は同期処理 setTimeout(function() { // この中の部分だけ非同期処理 console.log('resolve()'); resolve(); }, 1000); // この部分は同期処理 console.log('Promise() end'); }).then(function() { // この部分は非同期処理 console.log('then()'); }).catch(function() { // この部分は非同期処理 console.log('catch()'); }).finally(function() { // この部分は非同期処理 console.log('finally()'); }); // この部分は同期処理 console.log('-- 同期処理終了 --');
-- 同期処理開始 -- Promise() start Promise() end -- 同期処理終了 -- resolve() then() finally()
Promise の使用例
説明ばかりだとわかりづらいと思うので、実際に例を使用して、同期処理を非同期処理に変更してみましょう。
同期処理の場合
まずは下記のサンプルコードを見てください。 これは「何かめっちゃ重たい処理」を実行する関数です。 このサンプルコードを非同期処理に改造していきます。
console.log('-- 同期処理開始 --');
function veryHeavyFunc() {
console.log('前処理');
console.log('何かめっちゃ重たい処理1');
console.log('何かめっちゃ重たい処理2');
console.log('何かめっちゃ重たい処理3');
console.log('後処理');
}
veryHeavyFunc();
console.log('-- 同期処理終了 --');
-- 同期処理開始 -- 前処理 何かめっちゃ重たい処理1 何かめっちゃ重たい処理2 何かめっちゃ重たい処理3 後処理 -- 同期処理終了 --
非同期処理に変更する
先程のサンプルコードの「何かめっちゃ重たい処理」の部分を、 Promise を使用して非同期処理に変更してみます。
非同期処理されるのは、Promise の .then()、.catch()、.finally() の部分ですので、
resolve() を呼んで .then() の部分で処理することにします。
「後処理」の部分も「何かめっちゃ重たい処理」に続けて処理を行う必要があるので、.then() の中に入れます。
先程とは実行結果の順番が変わっていることに注目してください。 「何かめっちゃ重たい処理」が同期処理が終わった後に実行されているのが確認できるかと思います。
console.log('-- 同期処理開始 --');
function veryHeavyFunc() {
// この部分は同期処理
console.log('前処理');
return new Promise(function(resolve, reject) {
// この部分は同期処理
resolve();
}).then(function() {
// この部分は非同期処理
console.log('何かめっちゃ重たい処理1');
console.log('何かめっちゃ重たい処理2');
console.log('何かめっちゃ重たい処理3');
console.log('後処理');
});
}
veryHeavyFunc();
console.log('-- 同期処理終了 --');
-- 同期処理開始 -- 前処理 -- 同期処理終了 -- 何かめっちゃ重たい処理1 何かめっちゃ重たい処理2 何かめっちゃ重たい処理3 後処理
非同期処理を短く記述する
先程のサンプルコードの Promise のコンストラクタで resolve() を呼んでいる部分は、
もう少し短く記述することができます。
console.log('-- 同期処理開始 --');
function veryHeavyFunc() {
// この部分は同期処理
console.log('前処理');
return Promise.resolve().then(function() {
// この部分は非同期処理
console.log('何かめっちゃ重たい処理1');
console.log('何かめっちゃ重たい処理2');
console.log('何かめっちゃ重たい処理3');
console.log('後処理');
});
}
veryHeavyFunc();
console.log('-- 同期処理終了 --');
-- 同期処理開始 -- 前処理 -- 同期処理終了 -- 何かめっちゃ重たい処理1 何かめっちゃ重たい処理2 何かめっちゃ重たい処理3 後処理
非同期処理をさらに追加する
では今度は、呼び出し元の veryHeavyFunc() に注目してください。
この関数は戻り値として Promise が返ってきますよね。
そのため、非同期で処理されている「何かめっちゃ重たい処理」にさらに処理を追加することができます。
.then() の実行結果を、別の .then()、.catch() に引き継ぎたい場合は、return を使用して値を返すと良いでしょう。
受け取る場合は、コールバックの引数として受け取れます。
console.log('-- 同期処理開始 --');
function veryHeavyFunc() {
console.log('前処理');
return Promise.resolve().then(function() {
console.log('何かめっちゃ重たい処理1');
console.log('何かめっちゃ重たい処理2');
console.log('何かめっちゃ重たい処理3');
console.log('後処理');
return 'OK!!';
});
}
veryHeavyFunc().then(function(result) {
// 非同期処理をさらに追加することができる
console.log('実行結果:' + result);
console.log('追加処理');
});
console.log('-- 同期処理終了 --');
-- 同期処理開始 -- 前処理 -- 同期処理終了 -- 何かめっちゃ重たい処理1 何かめっちゃ重たい処理2 何かめっちゃ重たい処理3 後処理 実行結果:OK!! 追加処理
async キーワード
さて、前置きが長くなりましたが、ここからやっと async、await の説明になります。
まずは async キーワードから説明します。
async は非同期処理がある関数に付けるキーワードです。
このキーワードが付いた関数のことを「非同期関数」と言います。
このキーワードを付けておくことで、その関数に非同期処理が含まれていることを明示的に示すことができます。 たくさんの関数が定義されていた場合、どの関数に非同期処理が含まれているかわからなくなってしまって、その関数をどう扱って良いのか困ってしまうことがあります。 よくある解決策としては、関数名にプレフィックスやサフィックスを付けて、非同期処理が含まれているとすぐにわかるようにしたりします。 しかし、このキーワードが付いていれば、非同期処理が含まれていることが一目瞭然となります。
また、Promise を使用するとその関数の戻り値は Promise にしなければならないというルールがあるのですが、そのルールを強制してくれるようになります。 つまり、シンタックスレベルで絶対に Promise が戻り値で返ってくることを保証してくれるようになります。 非同期処理なのに Promise が返ってこないみたいな状況を防いでくれるようになります。 とはいえ、もし仮に Promise 以外を戻り値で返した場合、エラーになるわけではありません。 その戻り値が自動的に Promise に変換(ラッピング)されるようになるので注意してください。
先程までのサンプルコードでいうと、veryHeavyFunc() には非同期処理が含まれているので、
async キーワードを付けておくべきだということになります。
async function veryHeavyFunc() {
console.log('前処理');
return Promise.resolve().then(function() {
console.log('何かめっちゃ重たい処理1');
console.log('何かめっちゃ重たい処理2');
console.log('何かめっちゃ重たい処理3');
console.log('後処理');
});
}
await キーワード
await キーワードは、Promise を使った非同期処理を短く記述することができるようになるシンタックスシュガーです。
シンタックスシュガーですので、使っても使わなくても同じような動作をさせることができますが、使った方がよりコードが短くキレイに書けます。
このキーワードは非同期処理を短く記述するためのものなので、async キーワードが付いた非同期関数の中でしか使用できないので注意してください。
await キーワードを使用しない場合
await の動作はかなり説明しづらいので、簡単な例を使用しながら説明していきたいと思います。
まずは await が無い場合はどうなるか見てみましょう。
先程までのサンプルコードに別の処理を加えるために、wrapperFunc() を追加しました。
非同期関数を使用した関数もまた非同期関数になりますので、この wrapperFunc() にも async キーワードを使用しています。
veryHeavyFunc() を呼び出している部分に注目してください。
return と .then() の部分があまりにも Promise っぽい書き方で、直感的ではない書き方な気がしませんか?
console.log('-- 同期処理開始 --');
async function veryHeavyFunc() {
console.log('前処理');
return Promise.resolve().then(function() {
console.log('何かめっちゃ重たい処理1');
console.log('何かめっちゃ重たい処理2');
console.log('何かめっちゃ重たい処理3');
console.log('後処理');
});
}
async function wrapperFunc() {
console.log('別の処理1');
console.log('別の処理2');
return veryHeavyFunc().then(function(result) {
console.log('実行結果:' + result);
console.log('追加処理');
});
}
wrapperFunc();
console.log('-- 同期処理終了 --');
-- 同期処理開始 -- 別の処理1 別の処理2 前処理 -- 同期処理終了 -- 何かめっちゃ重たい処理1 何かめっちゃ重たい処理2 何かめっちゃ重たい処理3 後処理 実行結果:OK!! 追加処理
await キーワードを使用する場合
では次は await を使用した場合はどうなるか見てみましょう。
await を使用すると Promise の return と .then() の部分を省略することができます。
そしてなんと、await より上の部分は同期処理されて、await より下の部分は非同期処理されるようになります。
await という名前の通り、その部分で処理が止まっているイメージを持たれるかもしれませんが、実際の動作的にはその部分で関数が上下2つに分割されているイメージの方が近いでしょうか。
分割された上の部分を同期処理として実行し、下の部分を非同期処理として登録して関数を抜けています。
止まっているわけではないため、関数を抜けた後、後続に同期処理があればちゃんと実行されます。
かなり特殊な動きをしているのではないでしょうか。
非同期処理の実行結果は .then() のコールバックの引数として受け取っていましたが、
await を使用すると同期処理っぽく戻り値として受け取ることができます。
await を使用しないと戻り値は Promise になってしまいます。
また、Promise の .catch()、.finally() ではなく、
通常の try {}、catch {}、finally {} が使用できます。
非同期処理にもかかわらず、かなり同期処理っぽく記述できるようになるのではないでしょうか。
console.log('-- 同期処理開始 --');
async function veryHeavyFunc() {
console.log('前処理');
return Promise.resolve().then(function() {
console.log('何かめっちゃ重たい処理1');
console.log('何かめっちゃ重たい処理2');
console.log('何かめっちゃ重たい処理3');
console.log('後処理');
});
}
async function wrapperFunc() {
// await より上の部分は同期処理される
console.log('別の処理1');
console.log('別の処理2');
// await を使う
const result = await veryHeavyFunc();
// await より下の部分は非同期処理される
console.log('実行結果:' + result);
console.log('追加処理');
}
wrapperFunc();
console.log('-- 同期処理終了 --');
-- 同期処理開始 -- 別の処理1 別の処理2 前処理 -- 同期処理終了 -- 何かめっちゃ重たい処理1 何かめっちゃ重たい処理2 何かめっちゃ重たい処理3 後処理 実行結果:OK!! 追加処理