メニュー

JavaScriptの非同期処理を同期させる方法【初心者向け】


こんにちは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);

このコードでは下記の変更をしています。

・18行目のset_int()の引数に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を使う上で知っておきたいポイントがこちら。

・Promiseオブジェクト
・thenメソッド
・catchメソッド

それぞれ軽く説明します。

Promiseオブジェクト

Promiseオブジェクトは処理の完了を待たせる関数の戻り値となるように作成します。
サンプルコードでは6行目です。

Promiseオブジェクトには状態と呼ばれるものがあり、覚えておきたいのは下記の2つです。

・resolve:成功の状態(サンプルでは10行目)
・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()の中でreturnするとresolveになる
・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で実行される関数は必ずPromiseオブジェクトを返す必要がある
・awaitはasync function()の中でしか使えない

Promiseオブジェクトを返す関数はasync function()で作成できますが、awaitを使う関数もasync function()である必要があります。
サンプルコード19行目でrun_process()はPromiseオブジェクトを返していませんが、awaitを使うためにasyncを使っていると言う点に注意してください。


いかがだったでしょうか。
この記事を読んでくださった方の多くは非同期処理で困った経験がある方だと思いますので、この記事が少しで参考になったら嬉しい限りです。

この記事をシェアする

  • twitter
  • facebook
  • Google+
  • B!はてブ
  • pocket
トップへ戻る

CONTACT

ゲーム開発、Webサイト制作に関するご相談等ございましたら、お気軽にお問い合わせください。

ゲーム開発に関する
お問い合わせはこちら

Webサイト制作に関する
お問い合わせはこちら