IT戦記

ただただがむしゃらにソフト開発をしていたい

JavaScript1.7 の yield を使って、非同期処理を同期処理のように書く方法

経緯

id:kazuhooku さんが一年前にやってたことですが
Kazuho@Cybozu Labs: JavaScript/1.7 で協調的マルチスレッド
今日やっと挙動が理解できたのと、 Weave のソースを読んでいたらこのテクニックをバリバリ使っていて「ちょwwおまwww」ってなったので、自分でも作ってみようと思いました。
ほとんど id:kazuhooku さんのと同じものなので、既出です><本当にありがとうございました><

まず、 yield とは何か

yield とは、 JavaScript 1.7 から導入された機能です。
以下に yield の細かい挙動を示しておきます。

function f() {
  // なんかの処理
  yield;        // ... (1)
  // なんかの処理
  yield;        // ... (2)
  // なんかの処理
}

var g = f(); // この時点で f は実行されず、特殊なオブジェクトを返す
g.next(); // ここで (1) まで実行される
g.next(); // ここで (2) まで実行される
g.next(); // ここで f は最後まで実行されて、 StopIteration という例外を投げる

基本的にはこういう挙動です。つまり、yield を使うと関数の実行をチビチビ行えるのです。
next 以外にも send を使う方法もあります。 send は next のついでにチビチビ実行中の関数に値を渡すことができます。

function f() {
  // なんかの処理
  var value = yield;        // ... (1)
  // なんかの処理
}

var g = f(); // この時点で f は実行されず、特殊なオブジェクトを返す
g.next(); // ここで (1) まで実行される
g.send(10); // ここで (1) の変数 value に 10 を渡し関数実行の続きをする

こんな感じです。

ちなみに

JavaScript 1.7 は script タグの type を以下のように書く事で使う事が出来ます(Firefox only)

<script type="text/javascript; version=1.7"> // ここは 1.8 でも OK
// ここに JavaScript 1.7 を書く
</script>

次に、非同期処理って何?って話をします

JavaScript の非同期処理とは、誤解を恐れずに言い切ってしまえばコールバック関数を使う処理のことです。*1
例えば、以下のようなものが非同期処理です。

// ... (1)

xhr = new XMLHttpRequest;
xhr.open(method, url);
xhr.send(null);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {

    // ... (2)

  }
};

// ... (3)

これは、 XMLHttpRequest を使って HTTP のリクエストを行うコードです。これを実行するとどうなるでしょうか。
まず、 (1) の部分が実行されて、 XMLHttpRequest が構築されたあと (3) が実行されて、リクエストが終了した時点で (2) が実行されます。

このように

JavaScript の非同期処理は前後がむちゃくちゃになってしまうのです><
困ったさん><

というわけで、これを yield を使って解決してみましょう

まず、以下のような関数を作ります。
Function.prototype.do = function() {
    var g = this(function(t) {
        try { g.send(t) } catch(e) { }
    });
    g.next();
}
で、例えば、 XMLHttpRequest のラッパーを以下のように書きます。
Requester = function(resume) {
  this.resume = resume;
};

Requester.prototype.send = function(method, url) {
  xhr = new XMLHttpRequest;
  xhr.open(method, url);
  xhr.send(null);
  var resume = this.resume;
  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4)
      resume(xhr.responseText)
  };
};
そうすると、以下のように処理を書けてしまいます。
(function(resume) {

  var r = new Requester(resume);

  var json = yield r.send('GET', './hoge.json');
  alert(json);

  json = yield r.send('GET', './fuga.json');
  alert(json);

}).do()
おお!

まるで同期処理みたいですね!でも、心配無用!
ちゃんと裏側では非同期で通信していてスレッドを食いつぶしたりしません!
これはすごい!><

で、仕組みを簡単に解説

Function.prototype.do = function() {
  var g = this(  // ここの g は do された関数をチビチビ実行するためのオブジェクトで、 next や step という関数を持っている
    function(t) {  try { g.send(t) } catch(e) { } } // この関数は g の send を呼び出す。 ... (1)
  );
  g.next();
}

(function(resume) {.....}).do();  // resume はさっきの (1) の関数と同じ

なので、 do された関数の中で resume に対して値を渡して呼び出すと yield に値が帰るようになる。
つまり、こんな感じ

(function(resume) {
  // 1000 ミリ秒後に value に 100 という値が入る
  var value = yield setTimeout(function() { resume(100) }, 1000);
  alert(value);

  // 要素がクリックされたらその要素の innerHTML を取得
  var elm = document.getElementById('target');
  value = yield elm.addEventListener('click', function() { resume(elm.innerHTML) }, false);
  alert(value);

}).do();

おおおお
どうですか?><

まとめ

yield 楽しいよ><

*1:本当は違うけど、最初はそう思っていても問題ない