JavaScript での有限状態マシン: 第 2 回 ウィジェットを実装する

JavaScript と有限状態マシンを使用したブラウザー・アプリケーションの開発

連載第 1 回では、有限状態マシンを使用して単純な Web ウィジェット (ビューでフェードイン、フェードアウトする動画化されたツールチップ) の複雑な振る舞いを体系づけて設計する方法を説明しました。今回は、第 1 回で設計したウィジェットの振る舞いを JavaScript で実装し、連想配列や関数クロージャーをはじめとした JavaScript 言語独特の機能を最大限活用する方法について説明します。作成されるコードはコンパクトかつ簡潔でロジックもわかりやすく、その動画は負荷の高いプロセッサーでも滑らかに動作するものとなります。

Edward J Pring (pring@us.ibm.com), Senior Software Engineer, IBM 

Photo of Edward PringEdward Pring は、ニューヨーク大学でコンピューター・サイエンスの理学修士号を、スタンフォード大学で数学の理学士号を取得しています。IBM Research の一員として、オペレーティング・システム、パブリッシング・アプリケーション、メインフレーム用端末エミュレーター、パーソナル・コンピューター向けウィルス監視機能、Digital Immune System 対応のネットワーク・オートメーション、そして Web サービスの視覚化およびパフォーマンス分析など、広範な IBM 製品および技術に貢献しています。彼の特許ポートフォリオは、これらの分野すべてにまたがります。



2007年 2月 13日

第 3 回では、実用上の問題を取り上げ、この実装をよく使われているすべての Web ブラウザーで機能させるようにする方法を説明します。

有限状態マシンとは、設計者および実装者がネットワーク・アダプターやコンパイラーなどのイベント駆動型プログラムの複雑な振る舞いを体系づける方式のことです。今日、プログラマブル Web ブラウザーが新世代のアプリケーションに新しいイベント駆動型環境を提供しています。Ajax によって普及したブラウザー・ベースのアプリケーションが複雑さを増していくなか、有限状態マシンが提供する規則と構造を大いに利用できます。

第 1 回では、よく使われている Web ブラウザーに組み込まれた実装よりも手の込んだ振る舞いを持つ Web ページ用のツールチップ・ウィジェットを取り上げました。この FadingTooltip というウィジェットは、カーソルが HTML 要素の上にあるとフェードインし、ツールチップがしばらく表示された後にフェードアウトします。ツールチップはファードイン、フェードアウトする間もカーソルと連動し、カーソルが HTML 要素から離れるとフェードアウトし、HTML 要素の上に戻るとフェードインします。このような振る舞いには、FadingTooltip ウィジェットが多様なイベントに応答すること、そして場合によっては特定のイベントに対する望ましい応答が前のイベントに依存することが求められます。

開発者は、このようなイベント駆動型プログラムを体系づけるのに有限状態マシンの設計パターンを使っています。第 1 回では、有限状態マシンの設計規則を適用して、必要な振る舞いを定義する図 1 の状態表を作成しました。

図 1. FadingTooltip ウィジェットの状態表
図 1. FadingTooltip ウィジェットの状態表

状態表の行と列には、ウィジェットが応答するイベントの名前、そしてイベントの間でウィジェットが待機する状態がラベルとして付けられています。表の各セルが指定しているのは、特定の状態で特定のイベントが発生した場合にウィジェットが実行するアクションです。表のセルでは、アクションが実行された後にウィジェットが遷移する次の状態を指定することも、アクションの実行後にウィジェットが同じ状態を維持するように指定することもできます。空のセルは、その特定の状態では該当するイベントが発生しないことを示します。さらに第 1 回では、ウィジェットが異なるセルで関連アクションを実行できるようにするため、イベント間で記憶しなければならない状態変数のリストを作成しました。

第 3 回では、よく使われているブラウザーで実装をテストし、現実に起きてはならない状況に対処します。

第 1 回では有限状態マシンの実行環境には JavaScript が最適だとして、設計段階に関連する JavaScript の機能をいくつか挙げました。この記事では、設計を JavaScript に変換する方法、高度な言語機能を活用する方法、そして実装を厄介なものにする複雑で詳細な事項に対処する方法を具体的に説明していきます。

設計を JavaScript に変換する

有限状態マシンの設計は第 1 回で完了しているので、早速 JavaScript での FadingTooltip ウィジェットの実装に取り掛かれます。ここから、設計段階での気楽な抽象概念が実際の実行環境の厳しい現実にさらされることになります。

考慮に入れるのは、Netscape Navigator、Microsoft® Internet Explorer®、Opera、そして Mozilla Firefox といった広く使われているブラウザーの最新バージョンだけですが、この限られた数の実行環境でさえも十分頭痛の種です。実際にさまざまなブラウザーから JavaScript プログラムにマウス・イベントとタイマー・イベントをフックする際の細かい作業と格闘することになります。そこで救いの手を差し伸べるのが、関数クロージャーと呼ばれる JavaScript 言語の高度な機能です。また、もう 1 つの高度な機能として、状態表を直接コードに変換する連想配列も使用します。さらにこの記事では、HTML Division 要素を使ってツールチップを作成およびスタイル設定し、ツールチップにテキストと画像を入力してカーソル近辺に配置する手順、そしてビューでフェードイン、フェードアウトさせるとともにカーソルの動きと連動させる手順を紹介します。

でも何よりもまず、オブジェクト指向の開発で必要となるのは、実装するすべてのものを含めるオブジェクトです。そこで、まずはこのオブジェクトから取り掛かることにします。


あらゆるものを含めるオブジェクト

FadingTooltip ウィジェットには、Web 設計者がよく HTML ページにカット・アンド・ペーストする短い JavaScript コード・スニペットの一般的なものよりも、高度なプログラミング作業が必要です。ソフトウェア・エンジニアは、ウィジェットの変数とメソッドをオブジェクトにグループ化するという考えに違和感はないと思いますが、Java™ や C++ プログラミングの教育を受けた人々にとって、JavaScript オブジェクト・モデルは少々奇異に感じるかもしれません。ですが、JavaScript オブジェクトは FadingTooltip ウィジェットの必要を満たすには申し分ありません。JavaScript オブジェクトを使えば、変数とメソッドをオブジェクトにグループ化した上で、ツールチップごとに個別のデータ・インスタンスを作成できます。これらのオブジェクト・インスタンスは共通コードを共有しながらも、独立して実行します。

JavaScript では、オブジェクト・コンストラクターは単なる関数で、関数の名前がオブジェクトの名前となります。FadingTooltip ウィジェットは、フックしている HTML 要素と、そのツールチップに表示するコンテンツを認識しなければなりません。そこで、これらの事項をコンストラクターの引数として指定し、オブジェクトに保存します (ツールチップの振る舞いと外観に関するパラメーターを設定する手段も必要になるので、そのための引数も指定します。この引数は後で使用します)。変数には型がないため、オブジェクト・コンストラクターのコードはリスト 1 のように始まります。

リスト 1. FadingTooltip オブジェクト・コンストラクターの JavaScript コード
function FadingTooltip(htmlElement, tooltipContent, parameters) { 
    this.htmlElement = htmlElement; // save pointer to HTML element whose mouse events 
                                    // are hooked to this object
    this.tooltipContent = tooltipContent; // save text and HTML tags for the tooltip's 
                                          // HTML Division element
    ...

JavaScript では、コンストラクターが this.htmlElement および this.tooltipContent プロパティーに対して行うように、プロパティーに値を割り当てるだけで、オブジェクトの作成時または作成後の任意の時点でオブジェクトにオブジェクト・プロパティー (変数またはメソッド) を追加できます。

JavaScript でのオブジェクト・プロトタイプとは、オブジェクトの新しいインスタンスを作成するためのテンプレートのことで、オブジェクトの初期プロパティーとその初期値を定義します。オブジェクト・プロトタイプは、このウィジェットに必要な状態変数を第 1 回から持ってきた値で開始します (リスト 2 を参照)。

リスト 2. FadingTooltip オブジェクト・プロトタイプの JavaScript コード
FadingTooltip.prototype = { 
    currentState: null,    // current state of finite state machine (one of the state 
                           // names in the table below)
    currentTimer: null,    // returned by setTimeout, non-null if timer is running
    currentTicker: null,   // returned by setInterval, non-null if ticker is running
    currentOpacity: 0.0,   // current opacity of tooltip, between 0.0 and 1.0
    tooltipDivision: null, // pointer to HTML division element when tooltip is visible
    lastCursorX: 0,        // cursor x-position at most recent mouse event
    lastCursorY: 0,        // cursor y-position at most recent mouse event
    ...

オブジェクト・プロトタイプは、有限状態マシンに関するほとんどすべての事項 (状態表、アクション、パラメーター) を定義するのにふさわしい場所です。ただし、オブジェクト・コンストラクターを仕上げるには未解決の事項が 1 つ残っています。それは、カーソル・イベントのフックです。この記事の残りでは、カーソル・イベンドのフックを片付けてから、オブジェクト・プロトタイプの入力に専念することにします。


カーソル・イベントをフックする

第 1 回の設計段階で述べたように、ブラウザーは、カーソルが HTML 要素に重なったとき、要素内で移動したとき、そして要素から離れたときにイベントをJavaScript に渡すことができます。これらのイベントには、イベント・タイプやページ上でのカーソルの現在位置などの有益な情報が含まれます。ブラウザーがイベントを渡すには、前もって登録されている関数を呼び出します。残念ながら、これらの関数を登録する方法や関数に引数が渡される方法の詳細はブラウザーによって異なります。よく使われているすべてのブラウザーで有限状態マシンがカーソル・イベントをフックすることを確実にするためには、3 種類のイベント・モデルを実装しなければなりません。イベント・モデルごとのコードがかなり簡潔なことは幸いですが、その反面、コードの簡潔さには複雑性が隠されています。

Mozilla Firefox、Opera、そして最新バージョンの Netscape Navigator では、W3C (World Wide Web Consortium) が提案する標準イベント・モデルをサポートしています。イベント関数の登録 (および登録解除) が簡単で、登録された関数はブラウザーがチェーニングしてくれるので、この標準イベント・モデルが一番のお勧めです。標準イベント・モデルでは、HTML 要素の addEventListener メソッドを呼び出し、イベント・タイプ、そして該当するイベントが該当する HTML 要素で発生した場合に呼び出す関数を渡すことによって、カーソル・イベントをフックできます (リスト 3 を参照)。

リスト 3. カーソル・イベントをフックする JavaScript コード
function FadingTooltip(htmlElement, tooltipContent, parameters) { 
    ...
    htmlElement.fadingTooltip = this;
    if (htmlElement.addEventListener) { // for FF and NS and Opera
        htmlElement.addEventListener(
            'mouseover', 
            function(event) { this.fadingTooltip.handleEvent(event); }, 
            false);
        htmlElement.addEventListener(
            'mousemove', 
            function(event) { this.fadingTooltip.handleEvent(event); }, 
            false);
        htmlElement.addEventListener(
            'mouseout',  
            function(event) { this.fadingTooltip.handleEvent(event); }, 
            false);
    }
    ...

addEventListener を呼び出す際の 2 番目の引数は匿名関数 (名前のない関数) です。ここが、JavaScript で別の関数内に関数を定義する初めてのチャンスになりますが、他にもその機会はあるのでとりあえずはこのコードに慣れてください。JavaScript コードの任意の場所で function 関数を使えば、匿名関数をすぐに定義できます。function 関数が返す関数へのポインターは、他のあらゆる参照値と同じように使用できます。FadingTooltip ウィジェットでは、別の関数への関数ポインターを引数として渡し、ポインターが null でないかをテストし、変数に割り当て、オブジェクト・メソッドとして宣言します。

addEventListener メソッドに渡される匿名関数には、大した役目がないように見えますが、そうでもありません。カーソル・イベントが発生すると、ブラウザーが匿名関数を呼び出してイベント・オブジェクトを渡します。すると、匿名関数がそのイベント・オブジェクトを FadingTooltip オブジェクトの handleEvent メソッドに渡します。ブラウザーのイベント・オブジェクトには、カーソルの位置だけでなくイベント・タイプも含まれるため、1 つの handleEvent メソッドで、ウィジェットが応答する必要のあるすべてのカーソル・イベントを処理できます。

これらの単純な匿名関数は、目立ちはしないものの重要なもう 1 つのタスクを実行します。W3C イベント・モデルでは、HTML 要素の addEventListener メソッドに登録された関数がその要素のメソッドとなるため、ブラウザーがこれらの関数を呼び出すと、組み込み this 変数が対応する HTML 要素を指します。ただし handleEvent メソッドには、状態変数が含まれる FadingTooltip オブジェクトへのポインターが必要です。それには 1 つの方法として、FadingTooltip オブジェクトを指す fadingTooltip プロパティーを HTML 要素に追加し、このプロパティーに従ってオブジェクトの handleEvent メソッドを呼び出します。こうすると、handleEvent メソッドの実行時に this 変数が FadingTooltip オブジェクトを指すようになります。

Internet Explorer でのカーソル・イベントのフック

Microsoft Internet Explorer では現在、W3C で提案する標準イベント・モデルをサポートしていませんが、同じような独自のイベント・モデルを用意しています。その違いは以下のとおりですが、簡単に対処できます。

  • イベント・タイプがわずかに異なること
  • 登録された関数が HTML 要素のメソッドにならないこと
  • イベント・オブジェクトがグローバルな window オブジェクト内に残されること

このイベント・モデルでイベントをフックするには、HTML 要素の attachEvent メソッドを呼び出し、多少異なるイベント・タイプと関数を渡します (リスト 4 を参照)。

これが関数クロージャーを使用して変数を関数定義内に閉じ込める最初のチャンスになりますが、その機会は他にもあるので、ここでもとりあえずはこのコードに慣れてください。

リスト 4. Internet Explorer でカーソル・イベントをフックする JavaScript コード
function FadingTooltip(htmlElement, tooltipContent, parameters) { 
    ...
    else if (htmlElement.attachEvent) { // for MSIE 
        htmlElement.attachEvent(
            'onmouseover', 
            function() { htmlElement.fadingTooltip.handleEvent(window.event); } );
        htmlElement.attachEvent(
            'onmousemove', 
            function() { htmlElement.fadingTooltip.handleEvent(window.event); } );
        htmlElement.attachEvent(
            'onmouseout',  
            function() { htmlElement.fadingTooltip.handleEvent(window.event); } );
    } 
    ...

HTML 要素の attachEvent メソッドに登録された関数は、その要素のメソッドにはなりません。カーソル・イベントが発生すると、ブラウザーはこれらの関数を呼び出しますが、組み込み this 変数は HTML 要素ではなく、グローバルな window オブジェクトを指します。そのため、関数が HTML 要素に保管されたポインターから FadingTooltip オブジェクトを見つけることはできません。

幸いなことに、オブジェクト・コンストラクターの htmlElement 引数のレキシカル・スコープには匿名関数定義が含まれています。匿名関数定義内で htmlElement 変数を使うだけで、匿名関数でこの変数を囲むことができます。これが、関数クロージャーと呼ばれるものです。つまり、関数が別の関数内に定義されていて、内部関数が外部関数のローカル変数を使用している場合、JavaScript はこれらのローカル変数を内部関数の定義とともに保存します。すると、外部関数がリターンしてから内部関数が呼び出されたときに、内部関数は外部関数のローカル変数を引き続き使用できるというわけです。

この場合、JavaScript はコンストラクターがリターンした後も htmlElement 変数の値を維持するので、ブラウザーに呼び出された匿名関数は引き続き htmlElement 変数の値を使用できます。これにより、匿名関数が HTML 要素を見つけ、ブラウザーの助けがなくても FadingTooltip オブジェクトへのポインターに従うことができるようになります。

関数クロージャーは JavaScript 言語の機能であるため、W3C イベント・モデルを使用するブラウザーでも同じく有効に機能します。組み込み this 変数を使う代わりに、前のセクションで定義した匿名関数を使ってコンストラクターの htmlElement 引数の値を囲むという方法も可能です。

古いブラウザーでのカーソル・イベントのフック

W3C の標準イベント・モデルも Internet Explorer のイベント・モデルもサポートしていない古いブラウザーでイベントをフックするには、Netscape Navigator の初期のバージョンで提供された元のイベント・モデルを使用します。このイベント・モデルは一般に普及しているすべてのブラウザーでサポートされており、Web 設計者が Web ページを動画化するのにも広く使用されていますが、複数イベント・ハンドラーのチェーニングは行わないので、複雑なアプリケーションを実装する際にはできるだけ避けなければなりません。自分でイベント・ハンドラーをチェーニングするには、前に登録したイベント関数を独自のイベント関数の定義内に閉じ込め、handleEvent メソッドを呼び出してからイベント関数を呼び出します (リスト 5 を参照)。

リスト 5. 古いブラウザーでカーソル・イベントをフックする JavaScript コード
function FadingTooltip(htmlElement, tooltipContent, parameters) { 
    ...
    else { // for older browsers
        var self = this;
        var previousOnmouseover = htmlElement.onmouseover;
        htmlElement.onmouseover = function(event) { 
            self.handleEvent(event ? event : window.event); 
            if (previousOnmouseover) { 
                htmlElement.previousHandler = previousOnmouseover;
                htmlElement.previousHandler(event ? event : window.event); 
            }
        };
        ... and similarly for 'onmousemove' and 'onmouseout' ...
    }
}

上記の手法は完全ではありません。この手法では、ウィジェットが他のウィジェットでの処理が登録されたイベントを登録してイベントをチェーニングすることはできますが、他のイベント関数による登録解除はできません。その理由は、チェーニング・ポインターが他のイベント関数にアクセスできないためです。

このコードでは目先を変えて、コンストラクターの this 変数 (FadingTooltip オブジェクトを指定) を self という名前のローカル変数にコピーし、self ポインターを使って匿名関数定義に含まれる FadingTooltip オブジェクトを見つけています。FadingTooltip オブジェクトへのポインターは匿名関数定義内に閉じ込められているため、任意のブラウザーから呼び出された匿名関数はオブジェクトを直接見つけられます。この方法では、HTML 要素へのポインターを提供する際にブラウザーに依存する必要も、HTML 要素内に FadingTooltip オブジェクトへのポインターを保存する必要もありません。

W3C および Microsoft のイベント・モデル用に定義した匿名関数の中に FadingTooltip オブジェクトへのポインターを閉じ込めるという方法も一案です。こうすると、オブジェクトへのポインターを HTML 要素に保存する必要がなくなり、すべてのイベント・モデルで同じ手法を使って HTML 要素を見つけることができるようになります。ソース・コードのコンストラクターはこの方法でコーディングされています。

これで、よく使われているすべてのブラウザーでカーソル・イベントがフックされ、オブジェクト・コンストラクターも完成しました。ここからは、再びオブジェクト・プロトタイプを取り上げます。


タイマーを設定してタイマー・イベントをフックする

FadingTooltip コンストラクターは完成したので、今度はプロトタイプの入力に話題を移します。JavaScript では、オブジェクト・プロトタイプに変数だけでなくメソッドを含めることができます。メソッドは関数を指す単なる変数です。それでは手始めに、タイマーを起動およびキャンセルする汎用メソッドから取り掛かりましょう。

第 1 回の設計段階で説明したように、JavaScript にはワンショット・タイマーと繰り返しのティッカーという 2 種類のタイマーがあります。有限状態マシンにはこの両方のタイマーが必要です。タイマーを起動するには、setTimeout または setInterval 関数を呼び出し、時間の値 (ミリ秒)、そして timeout あるいは timetick が発生したときに呼び出すそれぞれの関数を渡します。この 2 つの関数によって返される不透明度の参照値は、後でタイマーをキャンセルする際に clearTimeout または clearInterval 関数に渡します。

ブラウザーは timeout 値に達した場合、あるいは timetick 間隔が発生するたびに、setTimeout 関数と setInterval 関数に引数として渡されたタイマー・イベント関数を呼び出します。ただし、timeout 関数と timetick 関数はオブジェクトのメソッドにはなりません。ブラウザーがこれらの関数を呼び出すと、this 変数がグローバルな window オブジェクトを指します。ブラウザーがタイマー・イベントに関する情報をこの 2 つの関数に渡すこともありません。

カーソル・イベントと格闘した後では、タイマー・イベントのフックは簡単なものです。タイマーを設定するときには、状態変数が含まれる FadingTooltip オブジェクトを指す組み込み this 変数を、setTimeout および setInterval 関数を呼び出す際のレキシカル・スコープ内にある self という名前のローカル変数にコピーします。次に、この self 変数を使用する匿名関数を定義し、これらの匿名関数を引数として setTimeout 関数と setInterval 関数に渡します。これにより self 変数は関数定義に閉じ込められるため、ブラウザーがこれらの関数を呼び出すと、self 変数が引き続き有効になります (リスト 6 を参照)。

リスト 6. タイマーを設定してタイマー・イベントをフックする JavaScript コード
FadingTooltip.prototype = { 
    ...
    startTimer: function(timeout) { 
        var self = this;
        this.currentTimer = 
            setTimeout( function() { self.handleEvent( { type: 'timeout' } ); }, 
            timeout);
    },
    startTicker: function(interval) { 
        var self = this;
        this.currentTicker = 
            setInterval( function() { self.handleEvent( { type: 'timetick' } ); }, 
            interval);
    },
    ...

タイマー・イベント関数の役割は、カーソル・イベント関数以上のものではありません。タイマー・イベント関数はイベント・タイプ (timeout または timetick) だけが含まれるありきたりなタイマー・イベント・オブジェクトを作成して、カーソル・イベントを処理する handleEvent メソッドに渡します。


アクション/状態遷移の表を作成する

JavaScriptでは、変数とメソッドだけでなく、配列やその他のオブジェクトなどのデータ構造もオブジェクト・プロトタイプに含められます。通常の配列に含まれる要素には整数でインデックスが付けられますが、連想配列の要素には数値ではなく名前でインデックスが付けられます。JavaScript における連想配列とオブジェクトは、構文が異なるだけでアクセスするデータは同じです。つまり、オブジェクト・プロパティーには連想配列要素としてアクセスできます (リスト 7 を参照)。

リスト 7. 連想配列要素としてのオブジェクト・プロパティーにアクセスする JavaScript コード
if ( htmlElement.fadingTooltip == htmlElement["fadingTooltip"] ) ... // always true

これを利用するには、状態表を関数の 2 次元連想配列として実装します。状態名とイベント名はそのまま配列のインデックスとして使用します。空でない配列のセルが指定するのは、ユーティリティー・メソッド (タイマーを起動するメソッドやキャンセルするメソッドなど) を呼び出してアクションを実行し、次の状態に戻る匿名関数です。handleEvent メソッドのコアが、リスト 8 のような配列構文を使ってこれらのアクション/状態遷移関数を呼び出します。

リスト 8. 連想配列に格納された匿名関数を呼び出す JavaScript コード
var nextState = this.actionTransitionFunctions[this.currentState][event.type](event);

handleEvent メソッドは、現行の状態とイベント・タイプをインデックスにして、連想配列としての actionTransitionFunctions 表にアクセスし、呼び出す関数を選択します。選択した関数には、引数としてイベント・オブジェクトを渡します。関数は必要なあらゆるアクションを実行してから、次の状態の名前を返します。

handleEvent メソッドは配列構文を使って actionTransitionFunctions 表にアクセスしますが、連想配列はオブジェクト (逆に、オブジェクトが連想配列でもあります) であるため、表の定義にはオブジェクト構文を使用できます。例えば、初期状態である Inactive で期待されるイベントは mouseover だけなので、この状態でこのイベントを処理する関数はリスト 9 のように定義できます。

リスト 9. 匿名関数をオブジェクト・プロパティーとして格納する JavaScript コード
FadingTooltip.prototype = { 
    ...
    initialState: 'Inactive',
    actionTransitionFunctions: { 
        Inactive: {
            mouseover: function(event) { 
                this.cancelTimer();
                this.saveCursorPosition(event);
                this.startTimer(this.pauseTime*1000);
                return 'Pause';
            }
        },
        ...

FadingTooltip オブジェクトのプロトタイプに含まれる actionTransitionFunctions プロパティーの値は、別のオブジェクトです。この別のオブジェクトには Inactive という名前のプロパティーがあり、その値も別のオブジェクトです。Inactive プロパティーに含まれるオブジェクトには、mouseover という関数を値に持つプロパティーだけがあり、この関数は、Inactive 状態で mouseover イベントが発生すると handleEvent メソッドによって呼び出されます。このメソッドは event という引数を取り、3 つのユーティリティー関数を呼び出して 3 つのアクションを実行した後、次の状態の名前として Pause を返します。この 3 つのアクションには、ブラウザーがマウス・イベント・オブジェクトに格納するカーソル位置の保存、タイムアウト値が pauseTime パラメーター (秒単位で指定されていますが、startTimer メソッドに必要なミリ秒に変換します) となっているタイマーの起動が含まれます。

Pause 状態のウィジェットが応答しなければならないのは、mousemove、mouseout、そして timeout という 3 種類のイベントです。Pause オブジェクトを定義する actionTransitionFunctions 表には、この 3 つのイベント・タイプごとにプロパティーがあります (リスト 10 を参照)。

リスト 10. Pause 状態でカーソル・イベントに応答する関数の JavaScript コード
FadingTooltip.prototype = { 
    ...
    actionTransitionFunctions: { 
        ...
        Pause: {
           mousemove: function(event) { 
              return this.doActionTransition('Inactive', 'mouseover', event);
           },
           mouseout: function(event) { 
              this.cancelTimer();
              return 'Inactive';
           },
           timeout: function(event) { 
              this.cancelTimer();
              this.createTooltip();
              this.startTicker(1000/this.fadeRate);
              return 'FadeIn';
           }
        },
        ...

Pause 状態で mousemove イベントが発生すると、handleEvent メソッドは単に doActionTransition メソッドを呼び出すだけの関数を呼び出し、event 引数を渡してその関数を返します。ご想像のとおり、doActionTransition メソッドは handleEvent メソッドと同様に、最初の 2 つの引数を配列インデックスとして使用して actionTransitionFunctions 表にアクセスし、表内で見つけた関数に 3 つ目の引数を渡します。mouseout イベントが発生した場合には、コードはこのセクションですでに起動されたタイマーをキャンセルする関数を呼び出してから、Inactive 状態に戻ります。

timeout イベントが発生した場合はと言うと、実行中のすべてのタイマーをキャンセルし、初期不透明度がゼロのツールチップを作成してティッカーを起動し、FadeIn 状態に遷移します。

actionTransitionFunctions 表の別の関数の例として、FadeIn 状態での timetick イベントを処理するための関数を定義します (リスト 11 を参照)。

リスト 11. FadeIn 状態でのタイマー・イベントに応答する関数の JavaScript コード
FadingTooltip.prototype = { 
    ...
    actionTransitionFunctions: { 
        ...
        FadeIn: {
            ...
            timetick: function(event) {
                this.fadeTooltip(+this.tooltipOpacity/(this.fadeinTime*this.fadeRate));
                if (this.currentOpacity>=this.tooltipOpacity) {
                    this.cancelTicker();
                    this.startTimer(this.displayTime*1000);
                    return 'Display';
                }
                return this.CurrentState;
            }
        },
        ....

FadeIn 状態では、timetick イベントが発生するたびに、handleEvent メソッドがツールチップの不透明度を少しずつ増やしていく関数を呼び出します。フェードイン期間 (秒単位で指定)、不透明度をゼロから増やしていくアニメーション・レート (1 秒あたりの段階数で指定)、そして最大不透明度 (0.0 から 1.0 の浮動小数点として指定) はすべてパラメーターです。この関数は現行の状態を返し、ツールチップの不透明度が最大不透明度パラメーターに達するまで有限状態マシンを FadeIn の状態に維持します。 最大不透明度パラメーターに達した時点でティッカーをキャンセルし、ツールチップを表示するためのタイマーを起動して Display 状態に遷移します。

actionTransitionFunctions 表に含まれる残りの関数についても同じように定義します。詳細に関しては、コメントが満載された完全なソース・コード図 1 と見比べてください。


イベント・ハンドラーを実装する

handleEvent メソッドは前もって何度も参照してきたので、このメソッドの実装は拍子抜けに思えるかもしれません (リスト 12 を参照)。

リスト 12. イベント・ハンドラーの JavaScript コード
FadingTooltip.prototype = { 
    ...
    handleEvent: function(event) { 
        var actionTransitionFunction = 
            this.actionTransitionFunctions[this.currentState][event.type];
        if (!actionTransitionFunction) 
            actionTransitionFunction = this.unexpectedEvent;
        var nextState = actionTransitionFunction.call(this, event);
        if (!this.actionTransitionFunctions[nextState]) 
            nextState = this.undefinedState(nextState);
        this.currentState = nextState;
    },
    ...

actionTransitionFunctions 表へアクセスするための実際の実装は、前のセクションでの提案とは異なります。handleEvent メソッドは、現行状態とイベント・タイプを連想配列のインデックスとして使い、actionTransitionFunctions 表から呼び出すべき関数を選択します。ただし、このメソッドは選択した関数を直接呼び出すのではなく、この関数へのポインターをローカル変数にコピーしてから、関数オブジェクトの call メソッドを使って関数を呼び出しています。これが可能なのは、関数オブジェクトは他の値とまったく同じように変数に割り当てることができるからです。そして、このようにしなければならない理由は、関数の実行中には組み込み this 変数が FadingTooltip オブジェクトを指す必要があるためです。前に提案したように配列インデックスを使って actionTransitionFunctions 表から直接関数を呼び出すと、this 変数は表内を指定することになります。一方、関数の call メソッドは、this 変数をその最初の引数に設定してから関数を呼び出し、残りの引数を渡します。

actionTransitionFunctions 表はすべてのセルが埋まっているわけではないことを念頭に置いてください。それぞれの状態で期待されるイベントに対する関数を定義し、残りのセルはすべて空白のままにしています。予期しないイベントについては、handleEvent メソッドは unexpectedEvent メソッドを呼び出して処理します。あるいはアクション/状態遷移関数が無効な状態の値を返すと、undefinedState メソッドを呼び出します。いずれのメソッドも実行中のタイマーをすべてキャンセルし、作成済みのツールチップがあれば削除して有限状態マシンを初期状態に戻します。この 2 つのメソッドはほとんど同じなので、リスト 13 に片方のメソッドを示します。

リスト 13. 予期しないイベント・ハンドラーの JavaScript コード
FadingTooltip.prototype = { 
    ...
    unexpectedEvent: function(event) { 
        this.cancelTimer();
        this.cancelTicker();
        this.deleteTooltip();
        alert('FadingTooltip received unexpected event ' + event.type + 
              ' in state ' + this.currentState);
        return this.initialState; 
    },	
    ...

これらのメソッドは、協力的なユーザーが問題の詳細をコードの作成者に送ってくれることを期待して、エラーを説明するアラート・ダイアログを表示します。


いよいよツールチップを表示する

実装するものは、ツールチップ自体を除いてもう何も残っていませんので、いよいよツールチップを実装します。

Pause 状態で timeout イベントが発生したときには、カーソル近辺にツールチップが表示されるようにしなければなりませんが、ブラウザーはカーソルの位置をタイマー・イベントに渡しません。幸いブラウザーはカーソル・イベントにはカーソルの位置を渡すので、カーソル・イベントの発生時に saveCursorPosition メソッドを呼び出して位置を状態変数に保管します (リスト 14 を参照)。

リスト 14. カーソル位置を保管する JavaScript コード
FadingTooltip.prototype = { 
    ...
    saveCursorPosition: function(event) {
        this.lastCursorX = event.clientX;
        this.lastCursorY = event.clientY;
    },
    ...

このツールチップは HTML Division 要素で、この要素の tooltipContent 引数のコンストラクターに渡されるテキスト、画像、マークアップが含まれます。リスト 15 に、この createTooltip メソッドを示します。

リスト 15. ツールチップを作成する JavaScript コード
FadingTooltip.prototype = { 
    ...
    createTooltip: function() {     
        this.tooltipDivision = document.createElement('div');
        this.tooltipDivision.innerHTML = this.tooltipContent;
        
        if (this.tooltipClass) {
            this.tooltipDivision.className = this.tooltipClass;
        } else {
            this.tooltipDivision.style.minWidth = '25px';
            this.tooltipDivision.style.maxWidth = '350px';
            this.tooltipDivision.style.height = 'auto';
            this.tooltipDivision.style.border = 'thin solid black';
            this.tooltipDivision.style.padding = '5px';
            this.tooltipDivision.style.backgroundColor = 'yellow';
        }
        
        this.tooltipDivision.style.position = 'absolute';
        this.tooltipDivision.style.zIndex = 101;
        this.tooltipDivision.style.left = this.lastCursorX + this.tooltipOffsetX;
        this.tooltipDivision.style.top = this.lastCursorY + this.tooltipOffsetY;
        
        this.currentOpacity = this.tooltipDivision.style.opacity = 0;
        
        document.body.appendChild(this.tooltipDivision);                
    },	
    ...

CSS クラス名をパラメーターとして指定すると、HTML Division 要素の外観に適用されます。指定しない場合は、デフォルトで基本スタイル設定が適用されます。ただし、このツールチップの振る舞いのいくつかの点は、ツールチップの位置や不透明度などといった外観に依存するため、スタイルシートで指定されている可能性があるプロパティーに関連する設定はすべてオーバーライドします。HTML Division 要素は最後に保存されたカーソル位置を基準とした絶対座標でページ上に配置され、他の重なり合ったすべての要素の上に表示されます。初期不透明度はゼロ、つまり完全に透明です。

fadeTooltip メソッドは、FadeIn または FadeOut 状態で timetick イベントが発生するたびに呼び出されます。このメソッドは、ツールチップの不透明度がゼロから最大不透明度の範囲内であることを確認しながら、不透明度を少しずつ増やすか、または減らします (リスト 16 を参照)。

リスト 16. ツールチップをフェードイン、フェードアウトする JavaScript コード
FadingTooltip.prototype = { 
    ...
    fadeTooltip: function(opacityDelta) { 
        this.currentOpacity += opacityDelta;
        if (this.currentOpacity<0) 
            this.currentOpacity = 0;
        if (this.currentOpacity>this.tooltipOpacity) 
            this.currentOpacity = this.tooltipOpacity;
        this.tooltipDivision.style.opacity = this.currentOpacity;
    },	
    ...

アクション/状態遷移関数にも、ツールチップを移動および削除するユーティリティー・メソッドが必要です。これらの実装は簡単明瞭なので、ソース・ファイルのコメントですべて説明しています。

今回の記事では何度も繰り返していますが、パラメーターを定義するまでは完全な実装とは言えません。これらのパラメーターはオブジェクト・プロトタイプのプロパティーですが、状態変数とは違ってデフォルト値があります (リスト 17 を参照)。

リスト 17. オブジェクト・プロトタイプのパラメーターを定義する JavaScript コード
FadingTooltip.prototype = { 
    ...
    tooltipClass: null,  // name of a CSS style to apply to the tooltip, or 
                         // 'null' for default style
    tooltipOpacity: 0.8, // maximum opacity of tooltip, between 0.0 and 1.0 
                         // (after fade-in, before fade-out)
    tooltipOffsetX: 10,  // horizontal offset from cursor to upper-left 
                         // corner of tooltip
    tooltipOffsetY: 10,  // vertical offset from cursor to upper-left 
                         // corner of tooltip
    fadeRate: 24,        // animation rate for fade-in and fade-out, in 
                         // steps per second
    pauseTime: 0.5,      // how long the cursor must pause over HTML 
                         // element before fade-in starts, in seconds
    displayTime: 10,     // how long to display tooltip (after fade-in, 
                         // before fade-out), in seconds
    fadeinTime: 1,       // how long fade-in animation will take, in seconds
    fadeoutTime: 3,      // how long fade-out animation will take, in seconds	
    ... 
};

オブジェクト・コンストラクターのオプション parameters 引数は、JavaScript Object Notation (JSON と呼ばれることもあります) でコーディングされたオブジェクトです。このオブジェクトは、あらゆるプロパティーのデフォルト値をオーバーライドできます (リスト 18 を参照)。

リスト 18. オブジェクト・コンストラクターのパラメーターを初期化する JavaScript コード
function FadingTooltip(htmlElement, tooltipContent, parameters) { 
    ...
    for (parameter in parameters) { 
        if (typeof(this[parameter])!='undefined') 
            this[parameter] = parameters[parameter];
    }
    ...
};

上記のコンストラクターは、parameters 引数に含まれるそれぞれのプロパティーを確認します。プロトタイプにそのプロパティーが存在する場合は、その値がパラメーターのデフォルト値をオーバーライドします。プロトタイプはオブジェクトなので、これも連想配列であることを忘れないでください。ここでも、オブジェクト表記を使ってパラメーターを定義し、配列表記を使ってパラメーターにアクセスしています。

以上で、FadingTooltip の実装は完了です。コンストラクターとプロトタイプについては、実際のソース・コードをダウンロードできます。


パフォーマンスについて一言

実装のテストに移る前に、パフォーマンスについて少々説明しておかなければなりません。

ブラウザーは JavaScript プログラムを同期して実行します。フックされたイベントが発生すると、ブラウザーはそのイベント・ハンドラーを呼び出し、イベント・ハンドラーから戻ってから次のイベントを続けます。イベント・ハンドラーが戻る前にさらにイベントが発生した場合、ブラウザーはそれらのイベントをキューに入れ、イベント・ハンドラーが戻ってからキューのイベントを 1 つずつ順番に同期して処理します。そのためイベント・ハンドラーが戻るまでに時間がかかると、フックされていないイベントに対するブラウザー自体の応答が遅くなる場合があります。この場合、ユーザーがプログラムの反応が遅いと感じたり、あるいはブラウザーが正常に動作していないと判断するおそれがあります。

イベント・ハンドラーをできるだけ簡潔にすることは常に重要ですが、間隔が短いタイマー・イベントで動画をシミュレートするようなプログラムではとりわけ重要になってきます。timetick イベント・ハンドラーの処理時間がティッカー間隔より長い場合、timetick イベントがブラウザーのイベント・キューに積み重なっていくため、プロセッサーが飽和状態になり、ブラウザーが応答しなくなってしまいます。

例えば、デフォルトのアニメーション・レートが 1 秒あたり 24 段階の場合、timetick イベント・ハンドラーがブラウザーに戻るまでに必要なすべてのことを実行する時間は、イベンド・ハンドラーがプロセッサーを独占していると仮定して最大 40 ミリ秒となります。最近のワークステーションでは、これだけの時間があれば大量の処理が可能です。目標としなければならないのは、与えられた時間内でできるだけ多くのことをこなすのではなく、プロセッサーの使用時間をできるだけ短くすることです。実装のプロセッサー使用率がわずかであれば、他のアクティビティーでプロセッサーに大きな負荷がかかっているときでも、動画は滑らかになり、プログラムの応答性も良くなるはずです。

モニターのリフレッシュ率に合わせれば動画化が滑らかになるかもしれないという考えで、アニメーション・レートを 1 秒あたり 60 または 85 段階に設定しようとはしないでください。このように設定すると、timetick イベントの間隔が約 12 ミリ秒に短縮されてしまいます。timetick イベント・ハンドラーの処理時間がこの間隔より長い場合、あるいはプロセッサーに対する競合がある場合、動画の動きがぎくしゃくしたり、ブラウザーが応答しなくなる可能性があります。


テストの準備ができました

実装が完了した今、実際のブラウザーでコードをテストする段階に来ました。コードのテストは第 3 回で取り上げます。ただしお忘れなく。開発とは繰り返しのプロセスなので、設計や実装の段階にまた戻らなければならないことは大いに考えられます。

参考文献

製品や技術を入手するために

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=250175
ArticleTitle=JavaScript での有限状態マシン: 第 2 回 ウィジェットを実装する
publish-date=02132007