Home > Latest topics

Latest topics > JavaScriptでsleepしたい、を実現する方法(require JavaScript 1.7)

宣伝1。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能! シス管系女子って何!? - 「シス管系女子」特設サイト

宣伝2。Firefox Hacks Rebooted発売中。本書の1/3を使って、再起動不要なアドオンの作り方のテクニックや非同期処理の効率のいい書き方などを解説しています。既刊のFirefox 3 Hacks拡張機能開発チュートリアルと併せてどうぞ。

Firefox Hacks Rebooted ―Mozillaテクノロジ徹底活用テクニック
浅井 智也 池田 譲治 小山田 昌史 五味渕 大賀 下田 洋志 寺田 真 松澤 太郎
オライリージャパン

JavaScriptでsleepしたい、を実現する方法(require JavaScript 1.7) - Feb 20, 2009

中野さんが、JavaScriptにはsleep(一定時間待ってから次の処理に進むという命令文)が無いせいでテストを書くのに難儀したという話を書かれているけれども。まさにこれをどうにかしたくて、UxUは進化してきたようなものと言える。

知ってる人は知ってるだろうけど、Firefox/Thunderbirdアドオン向けの自動テスト実行ツール・UxUでは、テストを書く上でsleepに相当する機能を利用できる。これは、JavaScript 1.7でジェネレータ・イテレータ機能を実現するために追加されたyield式のトリッキーな使い方だ。

これを実現しているのは、lib/utils.jsdoIteration()と、test/test_case.jsrun()ということになる。ここから要点だけを取り出すと、以下のような事をしている。

まず、スクリプトの書き手は、「sleepを使いたい処理」を関数として定義する。この時、sleepの代わりにyieldを使う。

var task = function() {
  doSomething1();
  yield 1000;
  doSomething2();
  yield 1000;
  doSomething3();
}

次に、スクリプトの書き手は、この関数を後述するdoIteration()に渡す。すると、doIteration()がよしなに計らって、「doSomething1()を実行した後、1000ミリ秒待って、doSomething2()を実行し、また1000ミリ秒待って、doSomething3()を実行する」という風な形で先程の関数の内容を実行する。

この時のdoIteration()の内容は、以下のような感じ。

function doIteration(aTask) {
  if (typeof aTask == 'function') {
    // 渡されたのが関数だったら、まず、評価した返り値を得る。
    aTask = aTask();
  }
  if (!aTask ||
      !('next' in aObject) ||
      !('send' in aObject) ||
      !('throw' in aObject) ||
      !('close' in aObject) ||
      aObject != '[object Generator]') {
    // 渡されたオブジェクトまたは関数の返り値が
    // ジェネレータ・イテレータではない場合、何もしない。
    return;
  }

  // ここからがミソ!

  // 全部の処理が終わったかどうか、を示すオブジェクトを定義
  var finishFlag = { value : false, error : null };
  var last = 0; // スリープ開始時点の時刻を保持する変数
  var sleep = 0; // スリープの長さ(秒数)を保持する変数
  var timer = window.setInterval(function() {
    if (
        // スリープの長さがちゃんと指定されていて
        sleep > 0 &&
        // スリープ開始時点からの経過時間がスリープとして
        // 指定された時間未満であれば
        (Date.now() - last) < sleep
       ) {
      // ここで処理を終えて、100ミリ秒後まで待つ。
      return;
    }
    // スリープとして指定された時間が経過したので、処理を進める。
    try {
      // 次にyieldが登場するまでの間の処理を実行。
      sleep = aTask.next();
      // next()の返り値はyield式に渡された値。
      last = Date.now(); // スリープ開始時刻を保持して
      return; // 処理を一旦終えて100ミリ秒後を待つ。
    }
    catch(e if e instanceof StopIteration) {
      // 最後のyield式の後の内容が実行されて、定義された関数の内容が
      // 最後まですべて実行されると、StopIteration例外が発生する。
      // よって、処理完了とみなす。
      finishFlag.value = true;
    }
    catch(e) {
      // それ以外の未知の例外が発生した時は、処理中断とする。
      finishFlag.error = e;
    }
    // 100ミリ秒ごとの繰り返し処理を停止。
    window.clearInterval(timer);
  }, 100);
  return finishFlag;
}

UxUの内部でやってる事は基本的にはこういう事。ただ、実際にはもうちょっと使い勝手をよくするために細かい処理が加わってる。

doIteration()が中で何をやってるのかを知らなければ、パッと見は、sleepという命令文の名前がyieldに変わっただけのようにすら見えるんじゃないだろうか。そこが、このやり方の狙いだ。スクリプトの書き手はタイムアウトだのコールバックだのといった難しい事を何も考えなくても良くて、単に「sleep文に相当する機能が加わったJavaScript」として好きなように処理を書く事ができる。テストを書くための工数が大幅に削減される(かもしれない)ので、テストを書くのが苦にならず、ばりばりテストを書けるようになる(はず)。その結果、充実したテストのおかげでより安心して開発に専念できるようになる(はず)。という理屈です。

ちなみに、勘のいい人は気付くだろうけど、setInterval()に渡している関数の冒頭に以下の内容を挿入すれば、doIteration()をいくらでも入れ子にできるようになる。

    if (
        typeof sleep == 'object' &&
        (!sleep.value || !sleep.error)
       ) {
      return;
    }
var task = function() {
  doSomething1();
  yield 1000;
  yield doIteration(function() {
          doSomething2();
          yield 500;
          doSomething3();
          yield doIteration(function() {
            doSomething4();
            yield 100;
            doSomething5();
          });
        });
  doSomething6();
};
doIteration(task);

さらに、こんなこともできる。

var task = function() {
  doSomething1();
  yield doIteration(function() {
          var flag = { value : false; }
          frame.addEventListener('load', function() {
            frame.removeEventListener('load', arguments.callee, false);
            flag.value = true;
          }, true);
          frame.contentDocument
               .defaultView
               .location.href = 'http://www.example.com/';
          yield flag;
          // フレームの読み込みが終わったらここに進む
          doSomething2();
        });
  doSomething3();
};
doIteration(task);

この辺をもっと簡単に書けるようにヘルパーメソッドを色々と整備したのがUxUのテスト実行環境、と思ってもらえれば大体それで正解です。

25日追記。他にも色々やり方があるようです。(どっちもMozilla限定だけど)

分類:Mozilla > 拡張機能 > uxu, , , , , , , 時刻:23:49 | Comments/Trackbacks (0) | Edit

Comments/Trackbacks

TrackBack ping me at


の末尾に2014年1月19日時点の日本の首相のファミリーネーム(ローマ字で回答)を繋げて下さい。例えば「noda」なら、「2009-02-20_sleep.trackbacknoda」です。これは機械的なトラックバックスパムを防止するための措置です。

Post a comment

writeback message: Ready to post a comment.

2014年1月19日時点の日本の首相のファミリーネーム(ひらがなで回答)

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のつぶやき

オススメ

Mozilla Firefox ブラウザ無料ダウンロード