チェ・ゲバムラの日記

脱犬の道を目指す男のブログ

【JS】vol. 1/3 データ取得(Deferredを使った非同期通信)〜Chapter4 ブレイクスルーJavaScript フロントエンジニアとして越えるべき5つの壁〜

1〜2は前に読んだので省略。
3はCanvasだからすぐには使わない。
ということでChapter4からスタートです。

このChapterでの目的は非同期通信処理でデータを取得、
ソート及びフィルタ機能をつけてHTMLに表示するサンプルプログラムを作ること。

データ操作の3ポイント

データの取得
Ajaxを使うことが多い。
便利だが、コールバック関数登録が必要で、複雑なネスト構造をつくりがち。

データ検索

データの表示
JSファイルにHTMLを書くことがあるが、大規模の場合は弊害があるため、
クライアントテンプレートを使う方が良い。

データを取得する

Deferredを使った非同期通信

非同期完了を待ってから実行するという場合に利用。
ただしPromiseはIE11でも非対応。(2018/3/17現在:Can I use... Support tables for HTML5, CSS3, etc
ここでは敷居の低いjQueryの$.Deferredを使う。

ダメな例:2秒後にAjax実行し、consoleで中身を確認する処理

setTimeout(function(){
  $.ajax({
     url:"data.json",
     success:function(res){
         console.log(res);
    }
  });
},2000);

可読性が低いのでNG。
これをDeferredを使って実装するが、その前にstateという概念を理解する。
・pending(何も処理されてない状態)
・resolved(処理が正常完了)
・rejected(処理失敗)

これを踏まえてDefererredで再実装してみる

var deferred = new $.Deferred();  //Deferredオブジェクト作成
setTimeout(function(){
  deferred.resolve();   //コールバック関数を実行したい箇所でresolveメソッドの実行
},2000);

deferred.promise().then(function(){ //promiseはDeferredのサブセット。resolve、rejectなど変化するstateメソッドを排除したオブジェクトを返す。そしてstateがresolvedになった時にthenメソッドの関数が実行される。
  return $.ajax({
    url:"data.json"
  });
}).then(function(res){
  console.log(res);
});

コード量が増えたが、可読性が上がった。

実際の使い所

コールバックパターン

最も基本となるパターン。
例:bodyをアニメーション後にconsole表示

var deferred = new $.Deferred();

$("body").animate({
  marginTop:100
},{
  duration:1000,
  complete:function(){
    deferred.resolve();
  }
});

deferred.promise().then(function(){
  console.log("done");
});  

状態保持パターン

例:document要素をクリックするたびにconsoleを表示。
ただし2回目以降は1秒処理を遅らせる。

var prevState = new $.Deferred().resolve().promise();//①最初はresolve(処理完了>)とする

function asyncFuncDef(){
  var deferred = new $.Deferred();//③現在のstateを定義(deferredはここでは常にpending)
  setTimeout(function(){
    deferred.resolve();//④クリックのたびにここにきて、1秒間はdeferredをpendingとする。
  },1000);
  return deferred.promise();
}

$(document).on("click",function(){
  prevState = prevState.then(function(){ // ②prevStateはクリック後1秒間はpendingになる。つまり連続クリックしてもすぐにdoneと表示されるのは最初のクリックのみ  
    console.log("done");
    return asyncFuncDef();
  });   
});

常にprevStateに状態を保持しているので、コメントに書いたとおり連続クリックしても1秒間は受け付けないような処理となる。
ちなみにconsole.log(deferred.state());で現在のstateが表示される。pendingとか。

可変長非同期通信パターン

可変長非同期とは、完了したい非同期処理の数が予測不能なこと。
複数の非同期通信が処理されてから次の処理を行いたい場合に便利。

例:全ての画像のロード完了を待つ処理

var deferreds = $("img").map(function(i,el){
  var deferred = new $.Deferred(),
    img = new Image();
  img.onload = function(){
    return deferred.resolve();
  };  
  img.onerror = function(){
    return deferred.resolve();
  };  
  img.src = el.src;
  return deferred.promise();
});

$.when.apply($,deferreds).then(function(){
  console.log("done");
}); 

.mapについて解説(配列をループせずに順番に処理)
Array.prototype.map() - JavaScript | MDN
.applyについて解説(参照値と引数で関数呼び出し)
Function.prototype.apply() - JavaScript | MDN

データ取得の実装

最初にJSONデータをAjaxで取得。
またDeferredを使って完了後に処理するようにする。

data.json

{
    "list": [
      {
        "id": 1,
        "name": "Alice",
        "age": 38,
        "group": "d"
      },
      {
        "id": 2,
        "name": "Bob",
        "age": 16,
        "group": "b"
      },
      {
        "id": 3,
        "name": "Carol",
        "age": 19,
        "group": "a"
      },
      {
        "id": 4,
        "name": "Charlie",
        "age": 34,
        "group": "d"
      },
      {
        "id": 5,
        "name": "Dave",
        "age": 16,
        "group": "a"
      },
      {
        "id": 6,
        "name": "Ellen",
        "age": 39,
        "group": "d"
      },
      {
        "id": 7,
        "name": "Frank",
        "age": 20,
        "group": "d"
      },
      {
        "id": 8,
        "name": "Eve",
        "age": 45,
        "group": "a"
      },
      {
        "id": 9,
        "name": "Isaac",
        "age": 30,
        "group": "c"
      },
      {
        "id": 10,
        "name": "Ivan",
        "age": 28,
        "group": "c"
      },
      {
        "id": 11,
        "name": "Justin",
        "age": 45,
        "group": "a"
      },
      {
        "id": 12,
        "name": "Mallory",
        "age": 39,
        "group": "d"
      },
      {
        "id": 13,
        "name": "Matilda",
        "age": 40,
        "group": "b"
      },
      {
        "id": 14,
        "name": "Oscar",
        "age": 19,
        "group": "d"
      },
      {
        "id": 15,
        "name": "Pat",
        "age": 17,
        "group": "c"
      },
      {
        "id": 16,
        "name": "Peggy",
        "age": 33,
        "group": "a"
      },
      {
        "id": 17,
        "name": "Victor",
        "age": 26,
        "group": "b"
      },
      {
        "id": 18,
        "name": "Plod",
        "age": 36,
        "group": "d"
      },
      {
        "id": 19,
        "name": "Steve",
        "age": 23,
        "group": "a"
      },
      {
        "id": 20,
        "name": "Trent",
        "age": 27,
        "group": "a"
      },
      {
        "id": 21,
        "name": "Trudy",
        "age": 19,
        "group": "c"
      },
      {
        "id": 22,
        "name": "Walter",
        "age": 36,
        "group": "c"
      },
      {
        "id": 23,
        "name": "Zoe",
        "age": 26,
        "group": "a"
      }
    ]
}
  function App(url) {
    var self = this;
    this.fetch(url).then(function(data) {
      self.data = data;
    }, function(e) {
      console.error("データの取得に失敗しました");
    }); 
  }

  App.prototype.fetch = function(url) {
    return $.ajax({
      url: url,
      dataType: "json"
    });
  };

new App("data.json"); 

$.ajaxの返り値はdeferred objectなのでthenメソッドが使える。