独り言

プログラミングの講師をしています。新人研修で扱う技術の解説と個人の技術メモ、技術書の紹介など

【JavaScript】非同期通信

JavaScriptの非同期通信についてのまとめ

そもそも非同期とは

非同期を知るには、同時に同期を知る必要があります。
イメージとしては電話とメールの違い。
通常の会話や、電話、テレビ会議などは、同期コミュニケーションです。
1人が話しているときは、他の人は聞き手に周り、話しての話が終わったら聞き手だった人が話す。
話している人が終わるのを待ち、終わった後に次の人が話し出すのが同期コミュニケーション。
一方で、メールやチャットなどは非同期コミュニケーションです。
相手が今どういう状態かは知らなくても、一方的に連絡をすることができます。
たとえ相手からの返信がなかったとしても、次のメッセージを送ることも可能です。
これが非同期のコミュニケーション。

コンピュータにおける同期・非同期も考え方は同じです。
一つの処理が終わるのを待ってから次の処理の進むのが同期処理。
一方で、処理が終わるのを待たずに次の処理まで進むのが非同期処理です。

非同期処理

JavaScriptでは、setTimeoutやsetIntervalなどの、関数を引数とにとる関数が多数ある。
引数として渡される関数のことをコールバック関数と呼ぶ。
JavaScriptではコールバック関数の処理は非同期処理となる。

通常、if文やfor文を使って作られたプログラムは、上から順に実行されていき、1つ1つ処理が終わってから次の処理に進む。
JavaScriptでは、コールバック関数の処理は非同期となるため、setIntervalやsetTimeoutなどの関数は、コールバック関数の処理が終わっていなくても次の行の処理に進む。
このような処理がJavaScriptの非同期処理。

サンプル

// 非同期処理
let count = 0;
let intervalId = setInterval(() => {
    count++;
    console.log(count);
    if(count >= 10) {
        clearInterval(intervalId);
    }
}, 1000);

let count2 = 0;
let intervalId2 = setInterval(() => {
    count2++;
    console.log(count2);
    if(count2 >= 10) {
        clearInterval(intervalId2);
    }
}, 1000);

結果は、1秒毎に1~10の数値が2個ずつ出力される。
それぞれのsetInterbal関数では、1秒毎に数値がカウントアップされて10まで出力される処理となっている。
同期処理の発想で考えると、1~10までが一つずつ出力されて、その後再び1~10までが出力されそうだが、実際には1~10がそれぞれ2個ずつ出力されるような結果になる。
それは、setIntervalの引数となるコールバック関数が終わるのを待たずに処理が進むため、結果としてコールバック関数の処理が非同期でそれぞれ独立して呼ばれているため。

setTimeoutとsetInterval

setIntervalを使うことで、指定したミリ秒毎にコールバック関数の処理を実行することができます。
一方、setTimeoutでは指定したミリ秒後に一度だけコールバック関数の処理が実行されます。
ですが、setTimeoutでも関数を再起呼び出しすることで指定したミリ秒毎に処理を実行することが可能です。

サンプル

function setNum1() {
    console.log('Hello');
    setTimeoutId1 = setTimeout(() => {
        setNum1();
    }, 1000);
}

// 関数の呼び出し
setNum1();

// setTimeoutの処理の終了
// clearTimeout(setTimeoutId1);

setIntervalで処理を何度も実行する場合とsetTimeoutで処理を何度も実行する場合の違いですが、 setIntervalでは、きっかり指定したミリ秒後に処理が実行され、前回の処理が終わっていなかったとしても、次の処理が実行されます。
一方でsetTimeoutでは、処理が終了した後に指定されたミリ秒後に次の処理が実行されます。
処理に負荷をかけたくない場合などでは、setTimeoutの処理が実行されるそうです。

非同期通信

JavaScriptを使うことで、非同期でWebサーバーに対してHTTPリクエストを投げてHTTPレスポンスを受け取ることができます。
非同期通信を使うことで、HTMLのaタグやformタグを用いて画面遷移をしなくても、単一のWebページで画面遷移せずにサーバーと通信することができます。
以前はJavaScriptでの非同期通信といえばAjaxという技術を使うのが主流でしたが、現在はfetchと呼ばれる関数を使うことで以前よりも簡単に非同期通信を実現できるようになりました。

従来の同期通信による処理の場合、サーバーと通信するたびに画面遷移して画面が表示されるまで処理を待つ必要がありました。
非同期通信の場合、必要に応じてページの一部だけを書き換えることが可能です。
また、非同期通信の場合はサーバーとの通信処理の最中でもクライアントの処理を継続することができます。
結果として非同期通信を用いることによってアプリケーションのパフォーマンス向上につながります。

非同期通信の例

  • Googleマップ
    Googleマップでは、画面遷移することなく、マウスをドラッグするだけで新たな位置情報を取得して画面を最新に表示することができます。あの仕組みは非同期通信が使われています。
  • 入力候補の表示
    GoogleYouTubeなどの検索欄では、入力した文字に応じて候補がいくつか表示されます。あの仕組みも非同期通信が使われています。
  • SNSのいいねボタン
    Twitterのいいねボタンなども、画面遷移することなく処理が実行されます。

fetchの例(GETの場合)

fetchを使った非同期通信の例です。
まずはGETの場合です。
ここではGETとPOSTについての細かい解説は割愛します。

javascript

fetch('sample.php?num1=10&num2=20')
.then(response => response.json()
    .then(json => console.log(json.result))
)
.catch(error => console.log(error))

sample.php

<?php
header("Content-Type: application/json; charset=utf-8");
$num1 = (int)$_GET['num1'] ?? '';
$num2 = (int)$_GET['num2'] ?? '';
$result = $num1 + $num2;
$data = ['result' => $result];
echo json_encode($data);

サンプルのjavascriptの処理が実行されると、ブラウザのコンソールに30が表示されます。
サンプルではサーバーサイドの言語はPHPで書いていますが、Webサーバー上で動作するプログラムであればなんでも構いません。

解説

まずfetchを使ってsample.phpに対して、GETでHTTPリクエストを送信します。
GETでリクエストを送信する場合はURLにパラメータが付与されます。
サンプルではnum1とnum2というパラメータで数値を送信します。

PHP側ではnum1とnum2のパラメータを受け取り、数値として加算処理をして、結果をjsonデータで返します。

JavaScriptでは、responseという名前で結果を受け取り、データをjsonに変換してコンソールに表示しています。

JSON

ここではJSONについても簡単に説明しておきます。
jsonとはデータのフォーマットの一つです。
以下のようなデータフォーマットです。

{
    name : 'Alice',
    age : 25,
    bloodTyoe: B,
}

キー : 値 という組み合わせのデータをカンマ区切りで並べて書き、全体を{}(中括弧)で囲います。

これまで非同期通信で主流だったAjaxでは、XMLと呼ばれるデータ形式でのデータもやりとりされていました。AjaxのxはXMLのxです。
しかし、XMLはタグを用いでデータを表現するデータ形式で、各データに対して開始タグ、終了タグが必要になり、テキストの量が増えます。
また、JSONの書き方はJavaScriptのオブジェクトの書き方と同じであり、JavaScriptとの相性が良いため、近年非同期通信ではJSONを使ってデータのやり取りが主流です。
サンプルでは、PHPのプログラムによって以下のようなJSONファイルをレスポンスとして返します。

{
    result : 30
}

thenとcatch

fetchではリクエストに成功した場合にはthenの処理が実行され、失敗した場合にはcatchの処理が実行されます。
ここでいう失敗は、ネットワークエラーなどでリクエストが送信できなかった場合です。
サーバー側でのプログラムのエラーなどではcatchの処理は実行されません。
サーバー側のプログラムでエラーが起きたかどうかはレスポンスのステータスコードなどを確認する必要があります。

thenとcatchの引数にはコールバック関数を指定します。
非同期通信に成功した場合にはResponseオブジェクトを得ることができるので、thenのコールバック関数ではResponseオブジェクト格納用の変数を指定します。
ここではアロー関数の形でresponseオブジェクトのjsonメソッドを実行しています。

Promise

thenとcatchはPromiseオブジェクトに大きく関係しています。
Promiseは非同期処理の完了を表すオブジェクトです。
fetchを使用した場合、Promiseが返ってきます。
Promiseで処理が完了した状態であればthen、失敗した状態であればcatchが実行されます。
また、レスポンスからJSONデータを取得するためにResponseオブジェクトのjsonメソッドを使用していますが、jsonメソッドもPromiseを返すため、さらにthenが入れ子の形になります。

fetchの例(POSTの場合)

続いてはPOSTの場合。
JavaScript

let data = new URLSearchParams();
data.append('num1', 100);
data.append('num2', 200);
fetch('sample.php', {
    method: 'POST',
    body: data,
})
.then(response => response.json()
    .then(json => console.log(json.result))
)
.catch(error => console.log(error))

PHP

<?php
header("Content-Type: application/json; charset=utf-8");
$num1 = (int)$_POST['num1'] ?? '';
$num2 = (int)$_POST['num2'] ?? '';
$result = $num1 + $num2;
$data = ['result' => $result];
echo json_encode($data);

結果はブラウザのコンソールに300と出力されます。

解説

PHPのコードは、リクエストがPOSTになったので値の取得で扱う変数が$GETから$POSTに変わります。
JavaScriptですが、POST送信するときにはfetch関数の第二引数でオプションを指定します。
第二引数の指定は省略可能ですが、デフォルトではGET送信になるため、POSTで送信する場合は第二引数の指定が必須になります。
POSTのでパラメータを送信したい場合はオプションのbodyに指定します。
bodyからデータを送信する場合にはURLSearchParamsのインスタンスに対してappendでデータ追加します。

Promiseって結局何?

Promiseは、非同期通信の完了を表すオブジェクトとのことでしたが、結局のところなんなん?と思っている人もいるのではないでしょうか。
以下のようなプログラムを考えてみます。

let data = new URLSearchParams();
data.append('num1', 100);
data.append('num2', 200);
let sum = 0;  // 変数の初期化
fetch('sample.php', {
    method: 'POST',
    body: data,
})
.then(response => response.json()
    .then(json => sum = json.result)
)
.catch(error => console.log(error))
console.log(sum);  // コンソールへの出力

変数sumを定義します。非同期通信で結果をsumに代入し、最後にコンソールに出力します。
sample.phpの処理でリクエストで送られたパラメータの値を合計し、結果として300が返ってくるとした場合、最終的にコンソールには何が表示されるでしょうか。
非同期処理のプログラムを作成したことがない人にとっては、直感的には300が出力されそうな気もしますが、結果は0が出力されます。
これはつまり、fetchの処理が非同期であるが故に、fetchの処理が終了する前にコンソールへの出力処理が実行されていることになります。
fetchの処理が確実に完了していることを保証した上で処理をしたい場合、thenの中に書くことで非同期処理が完了した前提で処理をすることができます。

参考サイト

HTTPリクエストとHTTPレスポンスについて
fetchの詳細
Promiseについて
Responseオブジェクトについて
Content-Typeについて

おまけ

iQueryを使用する場合

iQueryの場合は、ajaxというメソッドを使うことで簡単に非同期通信が利用可能。
サンプル。

$.ajax({
    url: $form.attr('action'),
    type: $form.attr('method'),
    data: $form.serialize(),
    dataType: 'json',
    beforeSend: function(xhr, settings) {
        // Buttonを無効にする
        $('.add-cart').prop('disabled', true);
    }
}).done(function(data) {
       // 成功したときの処理
}).fail(function(data){
       // 失敗したときの処理
}).always(function(data) {
       // 常に実行したときの処理
});

.doneがfetchの場合の.thenに、.failがfetchの.catchに該当するイメージ。

JavaScriptAjaxを使用する

JavaScriptAjaxを使用する場合は、XMLHttpRequestオブジェクトを使用します。
fetchに比べると複雑で、現在JavaScriptで非同期通信を行うのであればfetchを使うのがおそらく主流になるとは思いますが、他の人が書いたAjaxのソースを読む機会があったときのために概要を知っておくと良いかも。 サンプル

let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = () => {
        // 通信が完了した時
        if(xhr.readyState === 4) {
            // 通信が成功した時
            if(xhr.status === 200) {
                // console.log('通信成功');
                let json = JSON.parse(xhr.response);
                console.log(json.result);
            } else {
                console.log('通信失敗');
            }
    } else {
        // console.log('通信中');
    }
};
// リクエストを初期化
xhr.open('GET', 'sample.php?num1=10&num2=20', true);
// リクエスト送信
xhr.send(null);

axios

fetch, ajax以外で非同期通信を実現する技術としてaxiosと呼ばれる技術があります。
axiosの概要は以下

axiosとはブラウザやnode.js上で動くPromiseベースのHTTPクライアントです。

参考サイトはこちらになります。
https://www.willstyle.co.jp/blog/2751/

以前Vue.jsを使って開発を行なったときに使用しました。
Vue.jsでは非同期通信にはaxiosを使用するのが一般的なんだそう。
とはいえ、正直なところfetchと大きな差はなさそうなので、好みの問題かもしれません。

サンプル

// POSTの場合
params = new URLSearchParams();
params.append('type', 1); 
axios.post('./getUseTableList', params)
.then(res => this.tableList = res.data)
.catch(error => console.log(error));

// GETの場合
axios.get('./getUseTableList')
.then(res => this.tableList = res.data)
.catch(error => console.log(error));

fetchとの違いは、axiosの場合はGET送信の場合はgetメソッドを、POST送信の場合はpostメソッドを使用します。
非同期通信の結果のデータはレスポンスのdataに格納されます。

非同期関数を作る

functionを宣言するときにasyncキーワードをつけることで、非同期関数を作ることができます。
非同期関数ではPromiseオブジェクトを返却する必要があります。
非同期関数の中では、awaitキーワードを使用することができます。
awaitを使うことで、非同期関数の処理を一時停止してPromiseの解決を待つことができます。
非同期関数では、awaitを使うことで、Promiseチェーンを書き換えることも可能です。
詳しくは下記の公式サイトを参照ください。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function