※社内専用

【Education】JavaScriptあれこれ その3

非同期通信

ここではjQueryを使った非同期通信について述べる。
ちなみに、非同期処理のためだけにjQueryを使う人がいたくらいスタンダードな存在だったが
今は非同期専用ライブラリであるaxiosも人気がある。
ReactやVue.jsのようなview特化コンポーネントと組み合わせて使われることが多い。
ではAngularはどうかというと、こちらはフルスタックなので専用APIを持っている。
また、サーバーサイド(node.js)ではrequestモジュールの独占市場と言っても過言ではない。

非同期通信の基礎

http://www.hoge.jp のHTMLを取得するなら、以下のように書く。

$.ajax({
  url: 'http://www.hoge.jp',
  dataType: 'html'
}).done(function(html){
  // 引数:htmlにプレーンなテキストとしてHTMLがわたる
});

非同期なので、以下の内容の場合処理の順番は大体このようになる。
(console.logの番号通りの順番)

console.log(1);
$.ajax({
  url: 'http://www.hoge.jp',
  dataType: 'html'
}).done(function(html){
  // 引数:htmlにプレーンなテキストとしてHTMLがわたる
  console.log(3);
});
console.log(2);

コールバック地獄とは

上記で分かる通り、HTML取得後それをゴニョゴニョするような場合は
doneメソッドの中に処理を記述する必要がある。

では、その処理の成果物をもって別アドレスにPOST送信する場合はどうなるだろうか。

答えは以下の通り。

$.ajax({
  url: 'http://www.hoge.jp',
  dataType: 'html'
}).done(function(html){
  $.ajax({
    method: 'post',
    url: 'http://www.hoge.jp/form',
    data: { sample: html },
    dataType: 'html'
  }).done(function(html){
    // POST送信後の処理
  });
});

非同期をすればするほど、ネストが深くなるのが確認できる。
これを繰り返すのがコールバック地獄と呼ばれるものだ。
処理が複雑になればなるほど、ネストが深くどこかで関数化して逃がす必要が出てくる。
これはES2017のasync/awaitにて解消予定だが、使い方に若干のクセがあるため簡単に、とはいかない。

Promiseの説明の前に

2段階の非同期の処理が終わったら〇〇する関数として、上記を定義してみよう。

function acyncSample(url1, url2, callback){
  $.ajax({
    url: url1,
    dataType: 'html'
  }).done(function(html){
    $.ajax({
      method: 'post',
      url: url2,
      data: { sample: html },
      dataType: 'html'
    }).done(function(html){
      // 〇〇する
      callback(html);
    });
  });
}

使い方はこう。

let url1 = 'http://www.hoge.jp';
let url2 = 'http://www.hoge.jp/form';
acyncSample(url1, url2, function(html){});

ネストこそ発生するが、定義としてはそれほど悪くないように見える。
では、url以外に可変な情報が必要となり引数が増えたら?エラーハンドリングは?
実装してみよう。

function acyncSample(url1, url2, judge, done, fail){
  $.ajax({
    url: url1,
    dataType: 'html'
  }).done(function(html){
    $.ajax({
      method: 'post',
      url: url2,
      data: { sample: html, judge: judge },
      dataType: 'html'
    }).done(function(html){
      // 成功した場合〇〇する
      done(html);
    }).fail(function(){
      // 失敗した場合〇〇する
      fail('エラーメッセージなど');
    });
  });
}

引数がみるみる増えていくのが確認できる。
引数の順番にも気を使う必要があるし、メンテナンス性が高いとも言えない。
これは単純な処理の順番の保証でしかない。

Promise

これらを修正し、新しい概念を取り入れたのがPromiseだ。
Promise を使うということは、Promiseオブジェクトを返す ということとほぼ同義である。

Promiseは非同期に関わらず関数に定義することができる。
Promiseオブジェクトのインスタンスをreturnした関数を定義すればよい。

function acyncSample(url1, url2, judge){
  return new Promise(function(resolve, reject){
    $.ajax({
      url: url1,
      dataType: 'html'
    }).done(function(html){
      $.ajax({
        method: 'post',
        url: url2,
        data: { sample: html, judge: judge },
        dataType: 'html'
      }).done(function(html){
        // 成功した場合〇〇する
        resolve(html);
      }).fail(function(){
        // 失敗した場合〇〇する
        reject('エラーメッセージなど');
      });
    });
  });
}

上記の使い方はこう。

let url1 = 'http://www.hoge.jp';
let url2 = 'http://www.hoge.jp/form';
acyncSample(url1, url2, true)
  .then(function(html){}) // 成功時の処理
  .then(function(error){}); // 失敗時の処理

ネストはやはり発生するが、コールバックではなくメソッドにて処理するため記述はスッキリする。

これだけ見るとあまり恩恵は無いように見えるが、真価は処理が複数必要になったときに発揮される。

Promise.all()・・・並列処理

Promiseオブジェクトを返す関数を、配列として引数に渡すことで並列処理をし、全処理が完了後の処理を可能にするメソッド。
処理結果は取得順はなんであれ、引数として渡した配列の順番通りになるため非常にコントロールがしやすい。
上記のacyncSample 関数を、url2のみ変更して並列処理してみる。

let promises = [];
let pages = ['form', 'company', 'menu', 'case'];
let url1 = 'http://www.hoge.jp';
pages.forEach(page=>{
  let url2 = url1+'/'+page;
  promises.push( acyncSample(url1, url2, true) ); // 関数の配列を作る
});
Promise.all(promises).then(function(htmlArray){
  // htmlArrayには取得したhtmlが配列の順番通りに含まれている。
});

4ページ分の通信が発生していることになるが、並列なので処理にかかる時間は最も長い1ページ分でしかないため、パフォーマンスに大きく貢献できる。

直接(逐次)処理

逐次処理ともいうが、Promise.resolve()thenメソッドを繋げていくと逐次処理も実現できるらしい。
これは使ったことがないため、下記を参照する方が早い。

http://qiita.com/toshihirock/items/e49b66f8685a8510bd76
非同期処理を逐次処理で実行する