概要
サーバーからデータを受け取るアプリを作成した際にJavaScriptの順次実行に悪戦苦闘したので、その際に調べたことを本記事にまとめておきたいと思います。
目次
同期処理と非同期処理
JavaScriptでは、基本的に上から順に1つずつ実行されます。上から順に1つずつ実行することを同期処理と言います。次の sample1.js では、①を実行した後に②を実行しているので、以下のようになります。
sample1.jsconsole.log("start"); // ① console.log("end"); // ②
sample1.jsの実行結果start end
次の sample2.js では、APIからデータを取得しています。データをまとめて送るなどといった処理はすべてサーバ側で行われ、データが送られてきます。このように他の場所で実行してくれる処理を非同期処理と言います。(※ sample2.js 以降ではjQueryも使用しています。)
sample2.js$.ajax({ type: "GET", url: "APIのURL", data: "送信するデータ", dataType: "送信するデータ型", contentType: "受信するデータ型" // 成功時の処理 }).done(function(response, textStatus, jqXHR){ var message = "success"; console.log(message); });
sample2.jsの実行結果success
それでは、同期処理と非同期処理を混ぜてみます。次の sample3.js はAPIからデータを取得し、その成否を表示しています。
sample3.jsmain=()=>{ console.log("START"); // ① var message = getData("info"); // ② console.log(message); // ③ console.log("END"); // ④ } // APIからデータ取得 getData=(sendData)=>{ console.log("getData begin"); $.ajax({ type: "GET", url: "APIのURL", data: sendData, dataType: "受信するデータ型", contentType: "送信するデータ型" // 成功時の処理 }).done(function(response, textStatus, jqXHR){ var message = "success"; console.log("getData finish"); return message; }); }
sample3.jsの予想結果START getData begin getData finish success END
上から①、②、③、④の順に実行されると、上記の sample3.jsの予想結果 になります。しかし、非同期処理は実行を他の場所に任せている分、その間に次の処理を同時に行うことができ、並行処理のようになります。そのため、②でリクエストを送り、データを待つ間に、③や④を実行してしまいます。そのため、実際の実行結果は下記の sample3.jsの実行結果 になります。messageはgetDataが終わっていないため、何もないので、undefinedと表示されています。このように、非同期処理が終わるのを待たずに実行されてしまい、実行する順番が入れ替わってしまうことがあります。
sample3.jsの実行結果START getData begin undefined END getData finish
上記の予想結果のようにするにはどうすればよいのでしょうか?
順次実行
非同期処理を上から順に実行するには、Promiseとasync/awaitを使用するのがおすすめです。 sample3.js をPromiseとasync/awaitを用いて書き直したものが次の sample4.js になります。Promiseとasync/awaitを使用することで、awaitがついている関数の処理が終わるまで待ってくれます。
Promiseとasync/awaitの使い方:
1. 非同期処理をgetData(sendData)のように1つの関数にまとめておきます
2. 順次実行したい非同期処理や同期処理をasyncで囲みます(①)
3. 順次実行したい非同期処理の関数の前にawaitを書きます(②)
4. 関数にまとめた非同期処理をPromiseで囲みます(③)
5. 非同期処理が成功した時に返したい値などをresolveに入れます(④)
※入れられるのは1つだけなので、複数ある場合は配列にまとめてください
sample4.jsmain=()=>{ (async()=>{ // ① console.log("START"); var message = await getData("info"); // ② console.log(message); console.log("END"); })(); // ① } // APIからデータ取得 getData=(sendData)=>{ return new Promise((resolve, reject)=>{ // ③ console.log("getData begin"); $.ajax({ type: "GET", url: "APIのURL", data: sendData, dataType: "受信するデータ型", contentType: "送信するデータ型" // 成功時の処理 }).done(function(response, textStatus, jqXHR){ var message = "success"; console.log("getData finish"); resolve(message); // ④ }); }); }
sample4.jsの実行結果START getData begin getData finish success END
順次実行とエラー処理
しかし、このままでは、エラー処理がされていません。 sample4.js にエラー処理を追加してみたものが次の sample5.js です。
エラー処理の場合、asyncに囲まれている部分でエラーが発生した場合に行われる処理をcatchの中に書きます(①)。また、非同期処理の関数では、APIからエラーが返ってきた際の処理をAjaxの場合はfailに書きます(②)。そして、非同期処理が失敗した時に返したい値をrejectに入れます(③)。このrejectに入れた中身がasyncのcatchのerrの中身になります。
sample5.jsmain=()=>{ (async()=>{ console.log("START"); var message = await getData("info"); console.log(message); console.log("END"); })().catch((err)=>{ // ① console.log(err); console.log("ERROR END"); }); } // APIからデータ取得 getData=(sendData)=>{ return new Promise((resolve, reject)=>{ console.log("getData begin"); $.ajax({ type: "GET", url: "APIのURL", data: sendData, dataType: "受信するデータ型", contentType: "送信するデータ型" // 成功時の処理 }).done((response,textStatus,jqXHR)=>{ var message = "success"; console.log("getData finish"); resolve(message); // 失敗時の処理 }).fail((jqXHR, textStatus, errorThrown)=>{ // ② var message = "failed"; console.log("getData finish"); reject(message); // ③ }); }); }
sample5.jsの実行結果(成功時)START getData begin getData finish success END
sample5.jsの実行結果(失敗時)START getData begin getData finish failed ERROR END
複雑な順次実行
実はPromiseの中にasyncを書き、入れ子構造にすることができます。そのため、非同期処理の繰り返しも順次実行することができます。その一例を sample6.js に示します。
sample6.jsmain=()=>{ (async()=>{ console.log("START"); var message = await repeat(["info", "name"]); console.log(message); console.log("END"); })().catch((err)=>{ console.log(err); console.log("ERROR END"); }); } // 繰り返し処理 repeat=(words)=>{ return new Promise((resolve,reject)=>{ (async()=>{ console.log("repeat begin"); var multiMessage = []; for (var i=0; i<words.length; i++) { var message = await getData(words[i]); multiMessage.push(message); } console.log("repeat finish"); resolve(multiMessage); })().catch((err)=>{ console.log("repeat error"); reject(err); }); }) } // APIからデータ取得 getData=(sendData)=>{ return new Promise((resolve, reject)=>{ console.log("getData begin"); $.ajax({ type: "GET", url: "APIのURL", data: sendData, dataType: "受信するデータ型", contentType: "送信するデータ型" // 成功時の処理 }).done((response,textStatus,jqXHR)=>{ var message = "success"; console.log("getData finish"); resolve("success"); // 失敗時の処理 }).fail((jqXHR, textStatus, errorThrown)=>{ var message = "failed"; console.log("getData finish"); reject(message); }); }); }
sample6.jsの実行結果(成功時)START repeat begin getData begin // 1回目のgetData getData finish getData begin // 2回目のgetData getData finish repeat finish ["success", "success"] // 1回目のgetDataの結果と2回目のgetDataの結果 END
最後に
今回、社内イベントで初めて記事を書かせていただきました。拙いところが多々あるかと思いますが、改善点等ございましたら、教えていただけると幸いです。
最後まで読んでくださり、ありがとうございました。ヾ(。>﹏<。)ノ゙
参考文献
本記事を書くにあたり、参考にさせていただきました
- Qiita マークダウン記法 一覧表・チートシート
- Markdown記法 チートシート
- JavaScriptの同期、非同期、コールバック、プロミス辺りを整理してみる