何度やっても同じ

ただの日記

JavascriptでAJAXで取得した値を変数にキャッシュして使う

言葉で説明しにくいので、やりたいことをJavaで書くと、つまりこういうこと。

private Value value;
public synchronized Value getValue() {
  if (value == null) {
    value = loadValueFromExternalHost(); // 時間かかる
  }
  return value;
}

値があれば即座にそれを返すし、なければ値を取得し、それをキャッシュした上で返す。複数スレッドが同時にgetValueしようとしても、何度も外部ホストにアクセスしないよう、synchronizedで同期する。

これと同じようなことをJavaScriptでやりたいと思い、最初、下記のようなコードを書いてみたのだった(jQuery使用)。

var actWithValue = (function() {
  var value = null;
  return function(callback) {
    if (value == null) {
      $.ajax({
        method: "GET",
        url: "http://localhost/value",
        success: function(data) { value = data; callback(value); }
      });
    } else {
      callback(value);
    }
  };
})();

JavaScriptで、値をネットワーク経由でとってくる間、スレッドをブロックさせるわけにはいかないから、当然非同期アクセスを使うことになり、その関係で「値があれば返す、なければ取得して返す」ではなく、「値があればその値を使って即コールバック、なければ取得してコールバック」という関数になった。

けど、しばらくして(しばらく気付かなかったわけだが><)、これではまずいことに気付いてね。このactWithValue関数を何度も実行するとき、十分に間をおいてから呼べば問題ないのだけど、立て続けに呼ぶと、前回のAJAX呼び出しが完了してvalueに値がセットされる前に、何度もサーバにアクセスしてしまう。

試しに、実行に200ミリ秒かかるサーバ(以下)を準備して、

public class ValueController extends Controller {
  @Override
  public Navigation run() throws Exception {
    System.out.println("accepted a request at " + new Date());
    Thread.sleep(200);
    return forward("value.jsp");
  }
}

以下のような感じでactWithValueを10回繰り返し実行してみると、案の定、10回サーバにアクセスされてしまった。

$(function() {
  for (var i = 0; i < 10; i++) {
    actWithValue(function(value) {
      $("#console").append($("<p>" + value + "</p>"));
    });
  }
});

で、作り直したのが以下。

var actWithValue = (function() {
  var value = null;
  var xhr = null;
  return function(callback) {
    if (value == null) {
      if (xhr == null) {
        xhr = $.ajax({
          method: "GET",
          url: "http://localhost/value",
          success: function(data) { value = data; callback(value); },
          complete: function() { xhr = null; }
        });
      } else {
        setTimeout(function(){ actWithValue(callback); }, 100);
      }
    } else {
      callback(value);
    }
  };
})();

前回のactWithValueがまだ値を取得し終えていなかったら、100ミリ秒間をおいてリトライするというロジックを追加してみた。先ほどと同じ方法で10回繰り返し実行してみると、サーバアクセスは最初の一回だけ。うまく動いているように見える。

でもしかし、マルチスレッドプログラミングではないけれど、それに近い、バグの見つけにくい処理だと思うから、なにか問題がありそうだったら誰か指摘してほしかったりするのだった。それか、もっと簡単な方法ないかなあ……。


ついでに、呼び出した順番で実行されるのか気になったので、それも試してみた。前述の、確認用のスクリプトを下のように書き換え。ループ変数iを同時に表示するようにして、念のため実行回数を100回に増やした。

$(function() {
  for (var i = 0; i < 100; i++) {
    actWithValue((function(i) {
      return function(value) {
        $("#console").append($("<p>" + i + ":" + value + "</p>"));
      };
    })(i));
  }
});

これを実行すると……。問題なさそう。マルチスレッドではないのだから当然といえば当然か。

0:Hello World!
1:Hello World!
2:Hello World!
3:Hello World!
4:Hello World!
(中略)
96:Hello World!
97:Hello World!
98:Hello World!
99:Hello World!