〜宣伝〜
個人開発でエンジニア専門マッチングサービスを開発しましたので、是非未経験からエンジニア目指している人!現役エンジニアで教えたい人!使ってみてください!
β版リリース記念キャンペーン中です!
10名様限定、抽選でお好きな技術本1冊プレゼント!
🎉当選者にはメッセージ差し上げます(送付の際に住所はお聞きしません)
詳しくはこちらから↓
概要
- PHPで同期処理しか書いてこなかったので非同期処理がワケワカメだった。そんな自分がなんとかして非同期処理を理解してみたのでまとめてみる。
情報
- 今回紹介するコードは下記のサービスを使って実行し動作確認を行いました。
ベースとなるコード
-
今回、下記の様なjsのコードを用意しました。
console.log("--------------------"); function a() { console.log("関数aです"); b(); } function b() { console.log("関数bです"); c(); } function c() { console.log("関数cです"); } a();
-
関数aが関数bを呼び、関数bが関数cを呼んでいます。
-
上記を実行すると下記のようにコンソールに出力されます。
-------------------- 関数aです 関数bです 関数cです
関数aのconsole.logを1000ミリ秒置いてみる
-
ベースとなるコードを下記のように修正してみました。
console.log("--------------------"); function a() { setTimeout(function () { console.log("関数aです"); }, 1000); b(); } function b() { console.log("関数bです"); c(); } function c() { console.log("関数cです"); } a();
-
関数aが関数bを呼び、関数bが関数cを呼ぶ形は変わっていません。
-
変化は関数aの中で
console.log("関数aです");
の処理が1000ミリ秒待機してから実行されるようになったことです。 -
上記を実行すると下記のようにコンソールに出力されます。
-------------------- 関数bです 関数cです 関数aです
-
jsは非同期で処理が実行されます。関数bの実行完了を待たずして関数cが実行されるため出力順が「関数bです」→「関数cです」→「関数aです」の順になっています。
-
これは困ります。今回の関数aはconsole.logに値を出力しているだけなので、関数bやcが先に実行されても問題ありません。しかし、商用コードなどでは「関数aでAPIを実行し情報を取得、関数bやcでAPIから取得した値を加工」などのようにコーディングします。こうなると、関数aが完了する前に関数bやcが動いてしまうとエラーになります。
じゃあどうするの?昔はコールバック地獄だったらしい
-
コールバック関数とは「関数の引数になっている関数」のことです。下記のような、setTimeout()の第一引数になっているクロージャ(無名関数)をコールバック関数と呼びます。(クロージャじゃなくても「関数の引数になっている関数」はコールバック関数と呼びます。)
setTimeout(function () { console.log('foo'); }, 1000);
-
「関数aです」→「関数bです」→「関数cです」と出力してもらうには下記のようにします。
console.log("--------------------"); function a() { setTimeout(function () { console.log("関数aです"); b(); }, 1000); } function b() { console.log("関数bです"); c(); } function c() { console.log("関数cです"); } a();
-
「関数bの呼び出し位置が少し変わっただけじゃないか、、!」と思われた方もいらっしゃると思います。では下記はどうでしょう。。?関数bを関数aの2000ミリ秒後に実行するようにしてみます。
console.log("--------------------"); function a() { setTimeout(function () { console.log("関数aです"); setTimeout(function () { b(); }, 2000) }, 1000); } function b() { console.log("関数bです"); c(); } function c() { console.log("関数cです"); } a();
-
このように関数bを関数aの2000ミリ秒後に実行という要件が増えただけで入れ子が深くなり、複雑なコードになります。
Promiseの登場でコールバック地獄からの開放
-
このコールバック地獄からの開放をしてくれたのがPromiseオブジェクトです。
-
Promiseオブジェクトを使った書き方を下記に記載します。(ぱっとみわかりにくいと思いますので下で解説します。)
console.log("--------------------"); function a() { return new Promise(function (resolve, reject){ setTimeout(function () { console.log("関数aです"); resolve(); }, 1000); }); } function b() { return new Promise(function (resolve, reject) { setTimeout(function () { console.log("関数bです"); resolve(); }, 2000) }) } function c() { console.log("関数cです"); } const promise = a(); promise.then(b).then(c);
-
Promiseオブジェクトは3個の状態があります。
- pending: レスポンス待機中
- fulfilled: 解決完了(正常に処理成功しました!ドヤッ!って感じ)
- rejected: 失敗(処理に失敗しました。ショボーンって感じ)
-
さらに後述する
then()
やあとから出てくるcatch()
などはこのPromiseオブジェクトの状態により実行されたりされなかったりします。-
then()
: Promiseオブジェクトがfulfilledになった時に実行される。(ドヤッ!のときに実行される) -
catch()
: Promiseオブジェクトがrejectedになった時に実行される。(ショボーンのときに実行される)
-
-
PHPで言うところのtry catch文やmatch式に近いと個人的には感じています。
-
そしてPromiseオブジェクトはインスタンス化された場合、何も指定しなければずっとpending状態です。この場合
then()
もcatch()
も実行されません。 -
また、明示的に
resolve()
を呼び出すとfulfilled状態に、reject()
を呼び出すとrejected状態にする事ができます。 -
処理の流れ
- 関数aではPromiseオブジェクトをインスタンス化して返しています。インスタンス化時の引数にクロージャでsetTimeoutの命令とconsole.logの命令と
resolve()
が記載されています。 - 関数bもPromiseオブジェクトをインスタンス化して返しています。関数aと同様にインスタンス化時の引数にクロージャでsetTimeoutの命令とconsole.logの命令と
resolve()
が記載されています。 - 関数cは割愛します。
- 関数aを実行してインスタンスをpromiseというインスタンス変数に格納します。promiseからメソッドチェーンで実行してほしい順番に
then()
の引数に関数bの関数名と、関数cの関数名を入れて実行しています。
- 関数aではPromiseオブジェクトをインスタンス化して返しています。インスタンス化時の引数にクロージャでsetTimeoutの命令とconsole.logの命令と
-
先に記載されているコードは関数aも関数bも
resolve()
になっており、これは明示的に解決完了したよ!ということを表しています。 -
では明示的に
reject()
を指定してthen(c)
をcatch(c)
にしたらどうなるでしょうか?console.log("--------------------"); function a() { return new Promise(function (resolve, reject){ setTimeout(function () { console.log("関数aです"); // resolve(); reject(); }, 1000); }); } function b() { return new Promise(function (resolve, reject) { setTimeout(function () { console.log("関数bです"); resolve(); }, 2000) }) } function c() { console.log("関数cです"); } const promise = a(); promise.then(b).catch(c);
-
下記のように出力されるはずです。
-------------------- 関数aです 関数cです
-
これは関数aの戻り値がPromiseのrejected状態なので
then(b)
は実行されず、catch(c)
が実行されたということです。
非同期関数 async awaitを使う
-
Promiseオブジェクトのおかげでコード量は増えましたが可読性は比較的高いコードが書けるようになってきました!!いいねいいね!
-
メソッドチェーンで実行される関数が記載されていると一瞬でどの順番で関数が実行されているのか分かることは良いのですが、チェーンされているメソッドの間でちょっとした処理を挟みたいこともあります。そんな時はasync awaitが非常に便利です。
-
手始めに
then()
で呼び出しをしていたコードをasync awaitを使って書き換えてみます!console.log("--------------------"); function a() { return new Promise(function (resolve, reject){ setTimeout(function () { console.log("関数aです"); resolve(); }, 1000); }); } function b() { return new Promise(function (resolve, reject) { setTimeout(function () { console.log("関数bです"); resolve(); }, 2000) }) } function c() { console.log("関数cです"); } async function run () { await a(); await b(); await c(); }; run();
-
非同期関数はまずそのものをasyncキーワードにて宣言するようです。
-
次にPromiseの状態に応じて実行してほしい処理の前にawaitキーワードを記載します。
-
まとめると「これから非同期で処理しまーす」の宣言がasyncで「Promiseの状態をみて次を実行しまーす」の宣言がawaitって感じです!
-
後はtry catchなどを使ってPromiseがrejectedになった問の処理も記載しておきましょう!(あえて下記は関数bがrejected状態になるようにしています。そのため実行すると「関数aです」→「関数bです」→「rejected状態です」と出力されます。)
console.log("--------------------"); function a() { return new Promise(function (resolve, reject){ setTimeout(function () { console.log("関数aです"); resolve(); }, 1000); }); } function b() { return new Promise(function (resolve, reject) { setTimeout(function () { console.log("関数bです"); // resolve(); reject(); }, 2000) }) } function c() { console.log("関数cです"); } async function run () { try { await a(); await b(); await c(); } catch(e) { console.log("rejected状態です"); } };
run();
```
さいごに
- 非同期処理の超基礎部分を勉強してみました!どうしても同期処理しかやってこなかったので非同期処理に馴染めないところもありましたが超土台部分は理解できたかなって思います!
参考文献