こんにちはWEBの佐野です。
久しぶりの投稿になりますが、今回はJavaScriptの非同期処理についての内容になります。
この記事では非同期処理で想定した結果にならなかったケースを仮定して、その解決策を実際にコードを見ながら説明したいと思います。
ここで紹介する方法を詳しく理解するのは説明も長くなりますので、今回は使い方のイメージを掴める程度で紹介します。
よろしければ実際にコードを動かしながら最後までお付き合いください。
非同期処理とは
まず非同期処理ってどんなものかを簡単に説明します。
非同期処理は実行した処理の完了を待たずに次の処理を実行する動きの事です。
逆に同期処理は実行した処理の完了を待って次の処理を実行する動きとして説明していきます。
ここでは例として2つの変数を足し算するコードで説明したいと思います。
let int1 = 0;
let int2 = 0;
// int1とint2に値をセットする関数
function set_int(a, b){
int1 = a;
int2 = b;
}
// int1とint2を足し算する関数
function sum_int(){
alert(int1 + int2);
}
set_int(1, 2); // 変数に値をセット
sum_int(); // 足し算したものをアラートで表示
これを実行するとアラートで”3″が実行されることが解ります。
これくらいシンプルであれば非同期を意識することもないのですが、次のコードを実行するとどうなるでしょう。
let int1 = 0;
let int2 = 0;
// int1とint2に値をセットする関数
function set_int(a, b){
setTimeout(function(){ // 処理の実行に1秒の遅延
int1 = a;
int2 = b;
}, 1000);
}
// int1とint2を足し算する関数
function sum_int(){
alert(int1 + int2);
}
set_int(1, 2); // 変数に値をセット
sum_int(); // 足し算したものをアラートで表示
set_int()の呼び出しから処理の実行までに1秒の遅延が出る様にしました。
その結果、実行結果が”0″と表示されるようになります。
※結果が3の人は恐らくキャッシュなのでクリアしてください!
これはsum_int()が先に実行されたset_int()の完了を待たずに実行された結果で、この処理の完了を待たない動きが非同期処理となります。
同期処理を作る方法
次に先ほどの結果が”0″になったコードを”3″となるようにコードを組む方法を3つ紹介します。
・Promise
・async/await
コールバック
コールバックはかなりメジャーな方法です。
コール(実行)する関数の引数に処理(関数でも可)を追加して、必要な処理が終わった後に引数の処理を実行する方法になります。
let int1 = 0;
let int2 = 0;
// int1とint2に値をセットする関数
function set_int(a, b, callback){
setTimeout(function(){
int1 = a;
int2 = b;
callback();
}, 1000);
}
// int1とint2を足し算する関数
function sum_int(){
alert(int1 + int2);
}
set_int(1, 2, sum_int);
このコードでは下記の変更をしています。
・5行目で引数のsum_int()をcallbackの名前で受け取る(callbackの名前は任意)
・9行目で必要処理が終わった後にcallbackを実行
もちろん9行目を
callback() ⇒ sum_int()
にしても同様の結果になりますが、状況に応じて減算したいと言った場合にもコールバックの処理を変えるだけで対応できるので使い勝手が良い方法です。
Promise
『JavaScript 同期』とかで調べると必ず出てくるPromiseの紹介です。
こちらもコールバックと同様の事ができますが、書式がガラっと変わったり、Promiseが少し独特な仕様となっているので難しさを感じる人も多いはず。
しかし非同期処理を組む上では必須とも言えるので、ぜひ覚えておきましょう。
let int1 = 0;
let int2 = 0;
// int1とint2に値をセットする関数
function set_int(a, b){
return new Promise(function(resolve, reject){
setTimeout(function(){
int1 = a;
int2 = b;
resolve(); // 処理完了
// reject(); // 処理失敗
}, 1000);
});
}
// int1とint2を足し算する関数
function sum_int(){
alert(int1 + int2);
}
// 変数に値をセット
set_int(1, 2).then(function(){
// resolveだった時の処理
sum_int();
});
set_int(1, 2).catch(function(){
// rejectだった時の処理
alert("...err");
});
Promiseを知らない人には見慣れないコードかと思います。
コールバックとの使い分けとしては、処理が煩雑になり可読性が下がる場合に使用されたりします。
またここでは基本的な説明のみになりますが、他にも機能があるので状況に応じて使い分けてください。
その辺りを詳しく説明してくれているサイトを貼っておきますので興味ある方はぜひ。
話を戻してPromiseを使う上で知っておきたいポイントがこちら。
・thenメソッド
・catchメソッド
それぞれ軽く説明します。
Promiseオブジェクト
Promiseオブジェクトは処理の完了を待たせる関数の戻り値となるように作成します。
サンプルコードでは6行目です。
Promiseオブジェクトには状態と呼ばれるものがあり、覚えておきたいのは下記の2つです。
・reject:失敗の状態(サンプルでは11行目)
Promiseオブジェクトの中に記述している処理内で状況に応じてこの2つの状態に変化させる事で、その処理の完了を結果として返します。
thenメソッド
Promiseオブジェクトがresolve(成功)となった場合に実行したい処理を記述するためのメソッドです。
サンプルコードでは22行目です。
イメージとしてはコールバック関数を外に記述できるようにしたメソッドです。
catchメソッド
Promiseオブジェクトがreject(失敗)となった場合に実行したい処理を記述するためのメソッドです。
サンプルコードでは26行目です。
外部ファイルを参照する処理で、参照ファイルが無かった…など例外処理を記述したりします。
async/await
最後にasync/await、これはPromiseの使い勝手良くしたものです。
Promiseに比べかなりコードが見やすくなります。
let int1 = 0;
let int2 = 0;
// int1とint2に値をセットする関数
async function set_int(a, b){
/*
ここに時間のかかる処理がある想定
*/
int1 = a;
int2 = b;
return true;
}
// int1とint2を足し算する関数
function sum_int(){
alert(int1 + int2);
}
async function run_process(){
await set_int(1, 2);
sum_int();
}
run_process();
run_process()の中に実行するset_int()とsum_int()をまとめた形になりましたが、Promiseの時よりもコードがスッキリしました。
int_set()ではsetTimeoutで1秒の遅延を出していましたが、ここでは説明の都合上消しています。
async/awaitの挙動をそれぞれ説明します。
async
asyncは”async function()”で宣言します。
asyncを付けることで、その関数ではPromiseオブジェクト()を返すようになります。
ポイントはこんな感じです。
・async function()の中でthrowするとrejectになる
・async function()の中でしかawaitが使えない
上にも書いたようにasync function()はPromiseオブジェクトを返します。
asyncを使う場合の状態はreturnを使う事でresolve、throwを使う事でrejectになります。
また後述するawaitの説明にもなりますが、上記のコードでset_int()を以下に変更しても同じ挙動となります。
function set_int(a, b){
return new Promise(function(resolve, reject){
setTimeout(function(){
int1 = a;
int2 = b;
resolve();
}, 1000);
});
}
await
awaitはPromiseオブジェクトの状態が確定するまで処理を一時中断する動きをします。
サンプルコードでは20行目に使っています。
ここでは”await int_set()”としているので、int_set()の状態が確定(処理が完了)するまでsum_int()が実行されない挙動になります。
awaitを使用する場合のポイントはこんな感じです。
・awaitはasync function()の中でしか使えない
Promiseオブジェクトを返す関数はasync function()で作成できますが、awaitを使う関数もasync function()である必要があります。
サンプルコード19行目でrun_process()はPromiseオブジェクトを返していませんが、awaitを使うためにasyncを使っていると言う点に注意してください。
いかがだったでしょうか。
この記事を読んでくださった方の多くは非同期処理で困った経験がある方だと思いますので、この記事が少しで参考になったら嬉しい限りです。