JavaScriptの非同期処理をマスターする!Promise・async/awaitの使い方。
- 投稿日:2021.09.19 最終更新日:2023.07.04
- JavaScript実践
この記事では、JavaScriptの非同期処理について説明する。
非同期処理はJavaScriptの数ある難所の中でももっとも理解するのが難しい部分であり、ハマりやすいポイントでもある。
正しく理解できればそれほど難しいわけでもないので、JavaScriptをマスターしたいと考えているのであれば時間を掛けて勉強しよう。
JavaScriptにおける処理の実行
まずはJavaScriptにおけるコード処理の特徴やコンセプトを確認しよう。
JavaScriptはJavaやC++といったオブジェクト指向言語とは違った処理の特徴を持つ。
それらをまとめると以下のようになる。
- 基本的にシングルスレッド
- 非同期処理もよく使用される
- コードが上から下に実行されていく訳ではない
- 処理の順序をコントロールするためにPromiseなどの仕組みがある
JavaScriptはシングルスレッドのプログラミング言語だ。
「シングルスレッド」というと処理はコードの上から下に順番に行われていくように思えるがそうでは無い場合も多い。
実際のコードを例にして考えてみよう。以下のコードを見てみてほしい。
const log = function(){
console.log('fourth');
}
console.log('first');
setTimeout(() => {
console.log('second');
});
console.log('third');
log();
さて、このコードを実行するとどの順番でログが出力されるだろうか?
答えは以下の通りだ。
「undefined」を無視して考ると、first ⇒ third ⇒ fourth ⇒ secondの順で呼ばれたのが分かる。
なぜ”second”が最後に呼ばれたかというと、setTimeOut内のコードは他の処理がすべて終わった際に呼び出されるからだ。
これがJavaScriptの非同期処理の例だ。
非同期処理を使う場面
次にどんな場面で非同期処理が必要となるか考えてみよう。
まず、真っ先に挙げられるのがバックエンドと通信をおこなう時だ。
例えば写真のような比較的サイズの大きいデータをバックエンドから取得する場合、レスポンスが届くまでに数秒かかるような事も考えられる。
データが届くまでの間、他の処理をストップした場合は画面が更新するまでに数秒かかることになってしまう。
もちとん、そうなればユーザはストレスを感じずにはいられないだろう。
こういった処理の遅延を少なくするためにJavaScriptの非同期処理は必要である。
他にもJavaScriptでは、画面のレンダリングをおこなうタイミングを調整するなどの目的で非同期処理が使われる。
Promiseの使い方
次に本題となるPromiseオブジェクトについて見ていこう。
Promiseとは?
PromiseはJavaScriptのES6から導入されたオブジェクト。
先の章で例を挙げたようにJavaScriptでは非同期処理がよく使われる。
昔のJavaScriptではそれらの処理をコールバック関数を使用しておこなっていたが、それには2つの問題点があった。
- コールバックが連続する場合コードが複雑になる
- エラーが起きた際の処理が難しい
これらの問題点を解決するために導入されたのがPromiseだ。
一般的にPromiseという単語は日本語で「約束」と翻訳されるが、Promiseオブジェクトは処理の結果を約束するための仕組みだと考えるのが分かりやすいだろう。
Promiseを生成する
ここからはPromiseの使い方を見ていこう。
まずは結果をPromiseとして返すメソッドを作成しよう。
以下の例では、httpリクエストを行い取得したデータをPromiseの結果として返している。
public getUserInformationById(id: number) : Promise<User> {
return new Promise<User>((resolve, reject) => {
this._httpClient.request<User>('GET', 'https://sample.de', {body: id}).subscribe(result => {
if (result) {
resolve(result);
} else {
reject();
}
})
});
}
ここでは、resolveとrejectの使い方に注目してほしい。
取得したデータが正常であった場合はresolve()をデータとともに呼び、失敗だった場合はreject()を呼んでいる。
then/catch/finally
次にプロミスの使用例を見てみよう。
this.getUserInformationById(id).then(user => {
this.currentUser = user;
console.log('User info::' + user.name);
}).catch(error =>
console.log('Error by getting default user. ' + error)
);
上のコードは典型的なPromiseを使用したメソッドの呼び出しの例だ。
注目してほしいのはthenとcatchの部分。
thenとcatchはそれぞれ以下のような場合に呼び出される。
- then
メソッドの結果が成功だった際に呼び出される。メソッドの実行後の命令はこのブロックに記述する。
Promiseのステータスがfulfilledの時にthenが呼び出される。 - catch
メソッドの結果が失敗だった際に呼び出される。通常、このブロックにエラー処理をおこなう。
Promiseのステータスがrejectedの時にcatchが呼び出される。
メソッドの実行後には、thenかcatchのいずれかが呼び出される。
例えば、メソッドの実行のために一時的に作成したファイルを削除したい場合など、メソッドの結果に関わらずおこないたい処理がある場合はfinallyブロックを使用する。
thenの連鎖 Promise Chain
ひとつの構文の中でthenを続けて呼び出すこともできる。
これにより必要なる一連の処理を順序通りに読みやすく記述することができる。
fetchShoppingCart(userId)
.then(articles => chackAvalability(articles))
.then(articles => updatesPrise(articles))
.then(() => updateCache())
.catch(error => console.log('Error occured by checking shopping cart.' + error)
);
こういったthenが連鎖するパターンをPromise chainなどと呼ぶ。
なお、それぞれのステップごとにエラーを処理したい場合は以下のように、thenとcatchを交互に書いてもいい。
fetchShoppingCart(userId)
.then(articles => chackAvalability(articles))
.catch(error => console.log('Error occured by checking availability.' + error)
.then(articles => updatesPrise(articles))
.catch(error => console.log('Error occured by updateing prise.' + error)
.then(() => updateCache())
.catch(error => console.log('Error occured by checking shopping cart.' + error)
);
Promiseの応用
次に応用的なPromiseのパターンを見ていこう。
Promise.all
Promise.allは複数のプロミスがすべて完了した際に呼び出される関数。
それぞれのPromiseを変数として格納して、引数としてPromise.allに渡す。
const p1 = this._userService.getUserInformationById();
const p2 = this._articleService.this.updateShoppingCart();
const p3 = this.loadResources();
Promise.all([p1, p2, p3]).then(() => {
this._showLoginPage();
});
ログインなど大きなアクションの前に使用すると効果的。
asyc/await
asyncとawaitはES2017より導入された新しいキーワード。
プロミスを使った非同期処理をよりシンプルにおこなうために追加された機能だ。
private async _getDefaultUser(id: number) {
const user = await this.getUserInformationById(id);
this.updateInfoPanel(user)
}
上の例では、awaitキーワードを付けることにより、非同期処理であるgetUserInformationByIdが完了するまで待ってくれる。
関数の中でawaitキーワードを利用する場合は、宣言部分にasyncキーワードを加えなくてはいけない。
asyncキーワードを付加したメソッドは非同期処理となり、関数の結果はプロミスで返すこととなる。
参考書籍
この記事を書くにあたって、以下の書籍を参考にさせて頂きました。
JavaScript: The Definitive Guide, 7th Edition – O’Reilly