IT戦記 このページをアンテナに追加 RSSフィード Twitter

2008-03-03

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:本当は違うけど、最初はそう思っていても問題ない

ぱらっぱぱらっぱ 2008/03/04 08:40 すいません。教えてもらいたいことがあります。

Requester = function(resume) {
this.resume = resume;
};
これ↑を書いてないとエラーが出てくるんですが、その意味が理解できません(最小コードではRequester使わないのに)。

Function.prototype.do = function() {...} // 同じ内容
// -- ↓ここがないとdoの中でthisはfunctionじゃないと怒られる
function(resume) { // これは誰に結びつく?変数に入れても動くけど入れなくても動く。
this.resume = resume;
};
// --
(function(resume){
alert(”ready?”)
var value = yield setTimeout(function() { resume(100) }, 1000);
alert(value);
}).do();
↑これでOK押したあと1000msec後に100と表示されるのが不思議でたまらないんです。
doとresumeが頭の中で結び付かないです。

yieldがコンテキスト(内部)の維持をしつつ呼び出し元に復帰するというのと、prototypeを書き換えてゴニョゴニョするのが結びつかなくて、早起きまでする始末です。ゆっくり寝たいです。

YuichirouYuichirou 2008/03/04 11:59 amachang本人のスマートな解説に期待しつつ、とりあえず↓でyieldの本来の使い方を把握するところから始めるといいと思うよ。>ぱらっぱさん
http://developer.mozilla.org/ja/docs/New_in_JavaScript_1.7#Generators

ぱらっぱぱらっぱ 2008/03/05 08:43 ありがとうございます。>Yuichirouさん
さっそく”Generatorsとは?”からやり直してみました。
結論からすると先のRequester部がないと動かないというのは全くの勘違いでした。
失礼しました。
で、改めてジェネレータとイテレータと、クロージャを分けて考えてみることにしました。
最初の定義であるdo。
Function.prototype.do = function() {...}
これはつまり
・ここのthisはdoを実行してる無名のFunctionオブジェクト(以降f0)そのもの。
・gはそのfuncに対して新たな無名Function(以降f)を引数にしたイテレータ(iterators)。
・このgのジェネレータ(generators)はthis。
・んでtとはyieldのパラメータ(setTimeoutとelm.addEventLisner)。
と(分かりにくくてすいません)考えると、f0の引数resumeはf0そのもののイテレータであるのではないかと。

なので、あえてクロージャを使わずにグローバル変数で同じことを実装すると...。
var it;
var f = function(t) { try { it.send(t) } catch(e) { } };
Function.prototype.do = function() {
it = this();
it.next();// これで最初のyieldまで進む
};
var f0 = function() {
// 1000 ミリ秒後に value に 100 という値が入る
var value = yield setTimeout(function() { f(100) }, 1000);
alert(value);

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

こうなると思い、実行したらまさにそのままの結果でした(f0はクロージャでのが分かりやすいかも)。
ちゃんと仕様から確認することが大事だなと改めて思った次第です。
ありがとうございました。

はてなユーザーのみコメントできます。はてなへログインもしくは新規登録をおこなってください。