非同期処理
最終更新: 2022年01月15日
非同期処理とは
JavaScriptに限らずプログラミングは通常コードの上から下に順番に処理を実行していきます。つまり下の方に書かれたコードは前のコードがすべて終わるのを待ってから実行されます。
もし途中で10分ほど処理に時間のかかるコードがあったとしたら、それ以降のすべての処理は少なくとも10分以上待たないと実行されません。その状態を回避するために非同期通信が存在します。
非同期通信は自分の処理が終わるのを待たずに以降のコードを実行することができます。仮に10分かかる処理があったとしてもそれが非同期通信であれば、それ以降のコードはそれを待たずに順番に実行されます。
遅い処理が同期処理だった場合action1(); // はじめに実行される action2(); // action1の次に実行される slowAction(); // 10分かかる処理🔥 action3(); // slowActionを10分待った後に実行される⏳ action4(); // action3の後に実行される⏳ action5(); // action4の後に実行される⏳
遅い処理が非同期処理だった場合action1(); // はじめに実行される action2(); // action1の次に実行される asyncSlowAction(); // 10分かかる処理🔥 action3(); // asyncSlowActionの処理を待たずにすぐ実行される action4(); // action3の後に実行される action5(); // action4の後に実行される
ネットワーク通信を行う fetch
などは通常サーバーからのレスポンス(応答)を待つ必要があるため、非同期処理として振る舞うようになっています。これにより後続の処理が通信処理を待つ必要がなくなります。
しかし逆に結果を受け取ってから実行したいシーンではどうすれば良いでしょう?非同期処理には then(処理成功時に実行)、catch(処理失敗時に実行)、finally(成功失敗とわず処理終了時に実行)の3つのメソッドを持ちます。これらを使うことで非同期処理が終わったタイミングで結果を持って別の処理を実行することができます。
非同期処理の扱い
非同期処理は一般的にPromise(プロミス)と呼ばれるオブジェクトになります。Promiseは後述する then
や catch
などのメソッドを持ち、それらを使うことで非同期処理の結果を受け取ったり、処理が失敗した際のエラー対応をおこなったりします。
then, catch, finallyで結果を待つ
たとえばデータベースからユーザーデータを取得する getUser
という非同期処理を行う関数があったとします。非同期処理の結果を受けて何かしたい場合以下のように書きます。
getUser() // 処理成功時に走る .then(user => console.log(user)) // 処理失敗時に走る .catch(err => console.log(err)) // 処理終了時に走る .finally(() => console.log('処理終了'))
ここで注意が必要なのが、非同期通信である時点で処理自体は次の処理が優先されるため、上記のコード以降の処理は then
や catch
、 finally
より先に走るということです。
let user; // 処理終了後、上記変数にユーザーデータを代入 getUser().then(result => user = result); console.log(user); // undefined
上記の場合最後の console.log
は直前の非同期処理が終わるのを待つわけではないので、ここが走る時点では user
は未代入の状態です。ユーザーの中身を console.log
で出力したい場合 then
の中にコードを記述し、処理が終わった後に実行されるよう調整する必要があります。
async await を使う
もう一つの方法は await
を使う方法です。async
がついた関数内では非同期処理に await
をつけることができます。await
がついた非同期処理は処理が完了するまで待機してからそれ以降のコードを実行します。この書き方は then
catch
形式よりシンプルな見通しになるため現在主流となっています。
const action = async () => { const user = await getUser(); // ユーザーを取得するまで待ってから次の行を実行する console.log(user); // ユーザーが代入されている } action();
この書き方でエラーハンドリング(エラー時の対応)を行う場合以下のように書きます。
try { const user = getUser(); console.log(user); } catch(err) { console.log(err); } finally { console.log('処理終了'); }
複数の非同期処理(Promise)をまとめて処理する
たとえばユーザー3人のデータをリクエスト(非同期処理)して、全員の結果が揃ったタイミングで何かをしたいシーンがあります。 Promise.all
に非同期処理のリストを渡すと、それらの処理をすべて待ってから結果を配列として受け取ることができます。Promise.all
の結果はPromise
同様 then
catch
や async await
で処理できます。
// これらの結果をすべて待ちたい const task1 = getUser('1'); const task2 = getUser('2'); const task3 = getUser('3'); Promise.all([task1, task2, task3]) .then(users => console.log(users)); // [user1, user2, user3] // あるいは const users = await Promise.all([task1, task2, task3]); console.log(users); // [user1, user2, user3]
Promiseを作る
今では非同期処理はライブラリやAPI、各ツールのSDKが内部的に作成するため我々が自分でPromiseを作るシーンはほとんどありませんが、必要に迫られた時のためにPromiseの作り方を解説します。
// Promise(非同期処理)の作成 const getUser = (shouldSuccess) => return new Promise((resolve, reject) => { setTimeout(() => { if (shouldSuccess) { resolve('山田太郎'); } else { reject('データを取得できませんでした'); } }, 5000); }) } // 作成したPromiseを処理する→成功する getUser(true).then(user => console.log(user)); // 作成したPromiseを処理する→失敗する getUser(false).catch(err => console.log(err));
上記の例はPromiseを使って getUser
的な処理を模擬的に作ったものです。関数が実行されると5秒まってから結果が返却されます。
Promiseは new Promise
で作成し、その引数に resolve
と reject
を引数にとる関数を設定します(コールバック関数といいます)
resolve
は処理の成功を意味し、データを含めて実行者に応答します。 reject
は失敗を意味し、エラー内容を含めて実行者に応答します。
それぞれ resolve
は then
に渡され、 reject
は catch
に応答が伝わります。