問題
ユーザーがさまざまなタイプの数百の要素を同時に表示し、対話できるようにする大きなWebインターフェイスがあるとします。
オブジェクトのタイプごとに独自のクラスがあり、当然、インターフェイス内のユーザーアクションをこれらのクラスのメソッドに関連付ける必要があります。
この場合、簡単な例を考えます-オブジェクトには名前(nameプロパティ)があり、オブジェクト管理インターフェイスはこの名前が入力されるテキストフィールドです。 フィールドを変更する場合、オブジェクトのプロパティを変更し、新しい値をサーバーに送信します(SetNameメソッド)。
通常、典型的なアプリケーション初期化シーケンスは次のようになります。
- すべてのオブジェクトを初期化する
- インターフェースのDOMツリーを構築します
- 主要なインターフェイス要素(オブジェクトコンテナー、オブジェクト編集フォームなど)へのリンクを取得します
- オブジェクトプロパティの現在の値でインターフェイスを初期化する
- オブジェクトメソッドをイベントハンドラーとしてインターフェイス要素に割り当てる
額
単一オブジェクトの最も簡単な実装は次のとおりです。
function InitDomForObject(object){ // DOM, , object.container = $("<div>", {className : "container"}); object.inputs.name = $("<input>", {value : object.data.name}).appendTo(object.container); ... // object.inputs.name.change($.proxy(object, "SetName")); ... }
この実装の明らかな欠点は次のとおりです。
- 厳密に関連するレイアウトとJSコード
- DOMを構築し、イベントハンドラーを割り当てるための大量のコード(複雑なインターフェイス用)
- このアプローチでは、各オブジェクトのcreateElement、setAttribute、およびappendChildをループで数回呼び出すため、DOMの構築に時間がかかりすぎます。これらは非常に「重い」操作です。
パターン
このような困難に直面したため、テンプレートを使用し、DOMを手動で生成せずにすぐに思いつきました。HTMLコードを使用して文字列としてインターフェイスを構築すると、ブラウザによってほぼ即座に処理されることがわかっているためです。
HTMLを構築し(たとえば、サーバー側で)、ブラウザに出力しましたが、大きなDOMツリーで要素を探す必要があるという事実に直面しました。
ハンドラーを要素に割り当てる必要があるため、レイジー初期化を使用することはできませんが、アプリケーションをロードするときに、すべてのインターフェイス要素を完全に見つける必要があります。
さまざまなタイプのすべてのオブジェクトにエンドツーエンドの番号が付けられ、オブジェクトの配列に収集されているとします。
次の2つの方法があります。
オプションA
rel属性にオブジェクトIDを書き込むことにより、クラスで要素を検索します。
単一のオブジェクトのHTML表現は次のようになります。
<div class="container" rel="12345"> <input type="text" class="objectName" rel="12345" /> </div>
次に、インターフェイス要素のタイプごとに、このハンドラーのようなものを割り当てます。
$(".objectName").change(function(){ var id = $(this).attr("rel"); //, Objects[id].SetName(); // });
また、何らかの理由で各インターフェイス要素へのリンクを個別に保存する場合は、通常、オブジェクトの配列全体に不気味なサイクルを記述する必要があります。これは長くて不便です。
オプションB
もちろん、IDによる要素の検索ははるかに高速であることがわかっています。
idは一意である必要があるため、たとえば次の形式「name_12345」(「role_identifier」)を使用できます。
<div id="container_12345" class="container"> <input type="text" id="name_12345" class="objectName" /> </div>
ハンドラーの割り当てはほとんど同じに見えます:
$(".objectName").change(function(){ var id = this.id.split("_")[1];//, Objects[id].SetName(); // });
IDですべての要素を見つけることができ、ハンドラーが既に割り当てられているため、一度にすべてのリンクを収集するのではなく、必要に応じて(「遅延」)、すべてのオブジェクトのベースプロトタイプのどこかにGetElementメソッドを実装します:
function GetElement(element_name){ if(!this.element_cache[element_name]) this.element_cache[element_name] = document.getElementById(element_name + '_' + this.id); return this.element_cache[element_name]; }
IDによる検索はすでに非常に高速ですが、キャッシュによって誰も悩むことはありません。 ただし、ツリーからアイテムを削除する場合は、アイテムへのリンクがある限り、ガベージコレクターがアイテムに到達しないことに注意してください。
問題が1つだけあります。イベントハンドラーの大量の宛先コードです。インターフェイス要素のタイプごとに、イベントごとに個別のハンドラーを割り当てる必要があるためです。 予定の合計数=
最終決定
DOMのイベントの注目すべき特性を思い出してください。キャプチャとバブリング :
イベントハンドラをルート要素に割り当てることができるからです。すべて同じで、すべてのイベントがルート要素を通過するからです!
この目的のためにjQuery.liveメソッドを使用することもできますが、上記と同じこと、つまりオプションB 、つまりハンドラーの多数の宛先コードに到達します。
代わりに、イベント用の小さな「ルーター」を作成します。 イベントハンドラが不要な要素を除外するために、すべてのid要素を特殊文字で開始することに同意します。 ルーターは、この要素が「属する」オブジェクトにイベントをリダイレクトし、適切なメソッドを呼び出します。
var Router={ EventTypes : ['click', 'change', 'dblclick', 'mouseover', 'mouseout', 'dragover', 'keypress', 'keyup', 'focusout', 'focusin'], // Init : function(){ $(document.body).bind(Router.EventTypes.join(" "), Router.EventHandler); // }, EventHandler : function(event){ // if(event.target.id.charAt(0) != '-') return; var route = event.target.id.substr(1).split('_'); var elementRole = route[0]; var objectId = route[1]; var object = App.Objects[objectId]; // if(object == null) return; var method = object[elementRole + '_' + event.type]; if (typeof method == 'function') // { event.stopPropagation(); // return method.call(object, event); // , } } }
使用例:
<input type="text" id="-Name_12345">
SomeObject.prototype = { … Name_blur : function(e){ // blur this.data.name = e.target.value; this.GetElement("container").title=this.data.name;// title , id this.SaveToServer("name"); } }
ソリューションの利点:
- 多くの個別のハンドラー、最小コードを割り当てる必要はありません
- 各イベントは自動的に目的のオブジェクトメソッドにルーティングされ、メソッドは目的のコンテキストで呼び出されます。
- いつでもレイアウトにインターフェース要素を追加/削除できます。オブジェクトに適切なメソッドを実装するだけです
- ハンドラー割り当ての数は、イベントタイプの数に等しくなります( オブジェクトの 数 X 要素の数 Xイベントの数ではありません)
短所:
- ID要素を特別な方法で大量に割り当てる必要があります。 (これにはテンプレートの変更のみが必要です)
- 各要素の各イベントで、EventHandlerが呼び出されます(不要な要素をすぐに破棄し、stopPropagationも呼び出すため、パフォーマンスはほとんど低下しません)。
- オブジェクトの番号付けは、エンドツーエンドである必要があります(Objects配列を複数のオブジェクトタイプに1つずつ分割するか、サーバーと同じIDを使用する代わりに個別の内部インデックスを入力することもできます)
その他のオプション
クイックフィックス
インターフェイスが標準でシンプルな場合(つまり、標準コントロールのみを使用する場合)、通常のデータバインディング手法、たとえばjQuery DataLinkを適用します。
$("#container").link(object, { name : "objectName" } );
オブジェクトのプロパティを変更すると、テキストフィールドの値が変更され、その逆も同様です。
ただし、実際には、非標準のインターフェイス要素と、「オブジェクトの1つのプロパティに対する1つのインターフェイス要素」よりも複雑な依存関係を使用することがよくあります。 1つのフィールドを変更すると、一度に複数のプロパティに、異なるオブジェクトに影響する可能性があります。
たとえば、グループに属するいくつかの権限を持つユーザー( UserA )と、グループを選択できる要素( GroupAまたはGroupB )がある場合です。
次に、このリストの選択の変更には、他の多くの変更が伴います。
データ内:
- UserA.groupプロパティが変更されます
- UserAオブジェクトはGroupA.users配列から削除されます
- UserBオブジェクトはGroupB.users配列から削除されます
- 配列UserA.permissionsが変更されます
インターフェイスで:
- ユーザー権限のリストが変更されます
- グループ内のユーザー数を示すカウンターが変更されます。
等
このような複雑な依存関係は簡単に解決できません。 この場合、上記の方法で十分です。
同様のソリューション
VKontakteでも同様のアプローチが適用されました。イベントは、対応する属性(onclick、onmouseoverなど)を介して各要素に割り当てられます。 サーバーではなく、テンプレートのみがクライアント側で構築されます。
ただし、イベント処理はどのオブジェクトにも委任されません。
<div class="dialogs_row" id="im_dialog78974230" onclick="IM.selectDialog(78974230); return false;">
代わりに、グローバルオブジェクトのメソッドが呼び出されます。これは、たとえばアプリケーションでOOPアプローチが使用されている場合、あまり良くありません。
この原則を変更することもできますが、イベントを必要な方法に向けることはできますが、あまり美しくありません。
<div class="dialogs_row" id="im_dialog78974230" onclick="Dialog.prototype.select.call(Objects[78974230], event); return false;">
代わりに、このアプローチにルーター機能を適合させることができます。
<input type="text" id="Name_12345" onblur="return Route(event);">
function Router(event){ var route = event.target.id.split('_'); var elementRole = route[0]; var objectId = route[1]; var object = App.Objects[objectId]; // if(object == null) return; var method = object[elementRole + '_' + event.type]; if (typeof method == 'function') // { event.stopPropagation(); // return method.call(object, event); // , } }
これにより、「くしゃみ」のユーザーごとに多数のイベントを処理する必要がなくなりますが、必要なイベントを各要素のテンプレートで記述し、インラインコードでも記述できます。
これらの悪のどれが最も小さく、それに応じてどの方法を選択するかは、コンテキストと特定のタスクに依存します。