「APIでデータを取得したのに受け取れていない?」
「どうやら非同期処理というものが原因らしい」
「非同期処理を待ってから次の処理を実行したい!」
JavaScriptでプログラムを書いていると必ず対応しなければいけない問題が「非同期処理」です。
この記事では、非同期処理を伴う関数を正しく扱うための方法を、例となるコード付きで解説していきます。
目次
JavaScriptにおける非同期処理と同期処理
JavaScriptで関数やメソッドを実行した場合、以下の2パターンに分かれます。
- 同期的に処理される
- 非同期的に処理される
JavaScriptにおける同期処理とは
同期処理を一言で要約すると「実行完了を待つ処理」です。
例えば以下のような関数は同期処理となります。
// 同期処理をおこなう関数を宣言
const synchronousFunc = (value) => {
console.log("これは同期処理関数内のログです。")
return '完了!'
}
const message = '同期処理'
const result = synchronousFunc(message)
console.log(message + result)
実行結果は以下の通りです。
これは同期処理関数内のログです。
同期処理完了!
JavaScriptにおける非同期処理とは
一方、非同期処理を一言で要約すると「実行完了を待たずに次に進む処理」です。
非同期処理が実行中だとしても、次の処理が並行して走ります。
JavaScriptでは以下のような処理は非同期で実行されます。
- API通信
- データベース通信
- その他、実行完了までに時間のかかる処理
プログラムが外部と通信を行う際には、基本的に非同期となることを念頭に置いておきましょう。
実際のコードで見ると以下のようになります。
// 非同期処理をおこなう関数を宣言
const asynchronousFunc = (value) => {
const url = 'https://github.com/'
// APIへの非同期処理
fetch(url).then(res => {
console.log("これは非同期処理関数内のログです。")
return '完了!'
})
}
const message = '非同期処理'
const result = asynchronousFunc(message)
console.log(message + result)
実行結果は以下の通りです。
非同期処理undefined
これは非同期処理関数内のログです。
同期処理の時とログの順序が逆になりましたね?
また、asynchronousFunc()の実行結果であるresultがundefinedになっていることも分かります。
つまり、「非同期処理を伴う関数が実行完了する前に次の処理に進んだ」というです。
非同期処理のこの挙動はメリットでもありデメリットでもあります。
メリットは、「ユーザーを待たせない」ことです。
裏で勝手に処理させておこう、という考え方になります。
デメリットは、「データが存在しない」のような不完全な状態で次の処理を実行することです。
この記事にたどり着いた人は、おそらく後者のデメリットで悩んでいるのではないでしょうか?
それでは「非同期処理の完了を待ってから次の処理に進む方法」を2つご紹介します。
- promiseを使う
- async/awaitを使う
どちらとも、ES6(ECMAScript2015)から実装された機能です。
JavaScriptで非同期処理の完了を待つ方法1|promiseを使う
JavaScriptで非同期処理の完了を待つ1つ目の方法は、Promiseを使うことです。
非同期処理を行う関数・メソッドのなかでPromiseを使うと、実行完了時点の値をリターンするようになります。
Promiseは以下の3つの状態のいずれかを返します。
- pending: 初期状態。成功も失敗もしていない。
- fulfilled: 処理が成功して完了したことを意味する
- rejected: 処理が失敗したことを意味する。
そして、Promiseの使い方の例は以下です。
先ほどの、非同期処理を伴う関数を書き換えてみました。
// 非同期処理をおこなう関数を宣言
const asynchronousFunc = (value) => {
return new Promise((resolve, reject) => {
const url = 'https://github.com/'
// APIへの非同期処理
fetch(url)
.then(() => {
console.log("これは非同期処理が成功した時のログです。")
return resolve('完了!')
}).catch(() => {
console.error("これは非同期処理が失敗した時のログです。")
return reject('失敗!')
})
})
}
const message = '非同期処理'
// 非同期処理関数を実行してthenでチェインする
asynchronousFunc(message)
.then(result => {
console.log(message + result) // 非同期処理完了!と出力される
}).catch(error => {
console.error(message + error) // 非同期処理失敗!と出力される
})
実行結果は以下となります。
これは非同期処理が成功した時のログです。
非同期処理完了!
非同期処理を伴う関数が実行完了してから次の処理に進みましたね!
ポイントは以下です。
- 関数を
return new Promise((resolve, reject) => {})
という記法にすることで、Promiseを返す関数にする - 処理の成功時は、
return resolve(<成功時に返したい値>)
とする - 処理の失敗時は、
return reject(<失敗時に返したい値>)
とする asynchronousFunc().then(<実行結果> => {})
という記法で「チェイン」することで、非同期処理の関数の実行結果を受け取ってから次の処理に進む
非同期関数内の、resolve()メソッドはfulfilled状態のpromiseを返して、reject()メソッドはrejected状態のpromiseを返します。
非同期関数を実行した際のthen()メソッドは、fulfilled状態のpromiseが返ってきた場合に実行され、catch()メソッドはrejceted状態のpromiseが返ってきた場合に実行されます。
というのは理論的な話ですが、イメージで理解するなら「resolve()した値はthen()で受け取れる、reject()した値はcatch()で受け取れる」という感じでしょうか。
この他にもPromiseはいくつか用法がありますが、今回は割愛します。
JavaScriptで非同期処理の完了を待つ方法2|async/awaitを使う
次に紹介するのはasync/awaitを使った方法です。
個人的にはPromiseよりasync/awaitの方が好きです。
- 記述がシンプルになる(ネストが浅くなる)
- 直感的で分かりやすい
実際にはPromiseと組み合わせて使う状況もあります。
ですが、実行完了を待ちたい非同期処理が出てきたら「async/awaitで書けないか」というアプローチから始めます。
async/awaitは以下のような書き方になります。
// 非同期処理をおこなう関数を宣言
const asynchronousFunc = async(value) => {
const url = 'https://github.com/'
// APIへの非同期処理
return fetch(url).then(res => {
console.log("これは非同期処理関数内のログです。")
return '完了!'
})
}
// async付きの即時実行関数
const waitAsynchronousFunc = (async() => {
const message = '非同期処理'
const result = await asynchronousFunc(message)
console.log(message + result)
})();
これは非同期処理関数内のログです。
非同期処理完了!
async/awaitを使うときのポイントは以下です。
- 非同期関数(ここでは
asynchronousFunc()
)を定義する際にasyncをつけて宣言する - 非同期関数をawaitをつけて実行する
- awaitはasync付きの関数内でしか使えない
ちなみにES6のアロー関数と、functionを用いた関数宣言では、asyncをつける位置に違いがあります。
// functionによる関数宣言
async function asynchronousFunc() {
// 処理内容
}
// アロー関数による関数宣言
const asynchronousFunc = async() => {
// 処理内容
}
もう少し上記のコードを詳しく解説します。
asynchronousFunc()
の中ではfetchメソッドを使った非同期処理をおこなっているので、実行結果を待つためにawaitをつけて実行します。
ただし、awaitを使うためにはasync付きの関数内でしか実行できません。
なので今回は便宜上、waitAsynchronousFunc()
というasync付きの即時関数でラップしています。
こうすることで、非同期処理を伴う関数(asynchronouFunc()
)の実行結果を受け取ってから、次の処理に進んでいるということです。
なお、async付きの関数が実際にリターンしているのは、先ほど紹介したPromiseです。
なので、以下のようにawaitを使わずにthen()メソッドのチェインを使って実行結果を受け取ることも可能です。
// 非同期処理をおこなう関数を宣言
const asynchronousFunc = async (value) => {
const url = 'https://github.com/'
return fetch(url).then(res => {
console.log("これは非同期処理関数内のログです。")
return '完了!'
})
}
// async付きの即時実行関数
const message = '非同期処理'
asynchronousFunc(message).then(result => {
console.log(message + result)
})
以上がasync/awaitの使い方でした。
JavaScirptで非同期処理を制御する際の、ポイントを復習してみましょう!
- 通信や時間のかかる処理は非同期となる
- 非同期処理=「実行完了を待たずに後続に進む処理」
- 非同期処理の実行完了を待つにはPromiseを使う
- 非同期処理の実行完了を待つにはasync/awaitを使う
- async付き関数の返り値はPromise
- 無料・簡単・片手でホームページを作成できる自社サービス Rakwi
- Web制作とアプリ開発を学べるオンラインプログラミング講座 Upstairs
- 開発,DX推進支援サービス スタートアッププラン