自転車を発明し、FRPを知る方法

最近、インタラクティブホワイトボードと対話するためのWebアプリケーションを開始する機会がありました(!)サーバーとクライアントの両方のテクノロジスタック上のモバイルデバイス(!!)(!!!) プロトタイプ段階では、タスクはシンプルなグラフィカルエディターでした。 顧客は、何らかの方法で破線、円、セグメント、任意の曲線を描画し、テキストを追加できるようにしたいという要望を表明しました。 しかし、GoF、ファウラー、その他のすべての種類のパターンについての苦い経験から学んだことはすべて単純なように見えましたが、私はすぐに顧客がcであり、プロトタイプの1週間または1か月後に楕円、長方形、および他のニシュティヤクの山を描く必要があることに気付きました。 そして、これらすべては間違いなく異なる方法で行われなければなりません。 少なくともデスクトップとモバイル用。



実際、あなたはすべてを真っ先に行うことができます(プロトタイプの場合)が、週末は落ち、現在のプロジェクトのタスクが一時停止し、私はすべてを良い方法で行うことにしました。 そして最初の夜に- コールバック地獄



そして...

職場ではこれ以上することはないから







上の写真は、もちろん、そのエディターに基づいて作成されています。



美意識について



すぐに、描画ツールを記述するためのアイデアが頭に浮かび、コードが可能な限り技術的なタスクにできる限り対応するようになりました。 言いましょう



TK:
ユーザーとして、セグメントを描画できるようにしたい

1.左クリックは、セグメントの開始をマークします

2.マウスの左ボタンを押しながらクリックした後のマウスの動きは、中間結果を描画します

3.ボタンを離すと、セグメントの終わりがマークされます

4.データがサーバーに送信されます




真空中の球面コード:

myDrawingBoard .once(“mousedown”, setStartingPoint) .any(“mousemove”, drawLine) .once(“mouseup”, setEndingPoint) .atLast(saveFigure)
      
      







少なくとも、このコードは私の頭の中でどのように見えたかです。 今年の春、jQuery Russiaで似たようなものが見られました。そこでは実装がRx.jsに引き継がれました。 悲しいかな、私はビデオを見たりスピーカーと話す機会がなかったので、自分で車輪を再発明しなければなりませんでした。



同僚とおしゃべりした後、タスク自体がステートマシンであるという結論に達しました。 そして、私の自動コードには、この自動マシン上で少し魔術が必要です。イベントは定期的に存在するいくつかのノードで追跡する必要があるためですが、これらすべてのイベントからインターセプトする必要はなく、マシンの現在の状態で必要なイベントのみをインターセプトする必要があります。

実際、ノートブックを簡単に瞑想することで、まさにそのようなスキームを構築し、それを「フラットイベントチェーン」、つまりイベントのフラットチェーンと呼びました。



フラットイベントチェーン



各状態は、いわゆるメタイベントです。一連の繰り返しイベント(「any」タイプ)と終了イベント(「once」タイプ)で構成されるイベントの小さなチェーンです。 MetaEventに繰り返しイベントが存在しない場合は、終了イベントが存在する必要があります。存在しない場合は、いつこの状態を終了するかを判断できません。



メタイベント



このモデルでは、繰り返されるイベントが一般的なタイプの場合、衝突が発生する可能性があります。 これを行うには、チェーンの各要素に一意の名前が割り当てられ、メタイベントレベルで、使用するハンドラーがチェックされます。 このイベントの処理を担当する要素を決定するとすぐに、以前のハンドラーはすべて破棄されます。 単一の終了イベントがインターセプトされると、MetaEventは完了したと見なされ、マシンは次の状態に移行します。



実装について



チェーンの各要素はそのようなモジュールです:



 var BaseEvent = function (type, element, callback, context) { this.element = element; this.callback = callback; this.context = context; this.id = GuidFactory.create(); this.name = "me_" + this.id; if (type instanceof Object) { for (var key in type) { this._codes = type[key] instanceof Array ? type[key] : [type[key]]; type = key; this.element = $(document); break; } } this.type = type; this._uniqueType = type + "." + this.id; this._handlers = []; }; BaseEvent.prototype = { on: function (callback, context) { this._handlers.push({callback: callback, context: context || this}); return this; }, trigger: function () { for (var i = 0; i < this._handlers.length; ++i) { var obj = this._handlers[i]; obj.callback.apply(obj.context, arguments); } }, init: function () { var _this = this; this.element.on(this._uniqueType, function (evt) { if (!_this._codes || _this._codes.indexOf(evt.keyCode) >= 0) { _this.trigger(evt); } }) }, dispose: function () { this.element.off(this._uniqueType); } };
      
      







BaseEventは、initメソッドによる初期化(クライアントイベントへのサブスクリプションのアクティブ化)およびdisposeによるリソースの解放の可能性を想定しています。 ご覧のとおり、イベントは「eventType」スタイルと{{eventType»style:[keyCode]}の両方で表記されています-最後のオプションは、目的のkeyCodeが送信されたイベントのみをインターセプトします(1つのみが必要な場合、配列を書き込まないでください)。



チェーンはこのように記述されています:



 var MetaEvent = function () { this._events = []; this._closingEvent = null; this._currentEvent = null; this.closed = false; this.id = GuidFactory.create(); this.name = "me_" + this.id; }; MetaEvent.prototype = { push: function (evt) { if (this.closed) throw new Error("Cannot push event to closed MetaEvent"); this._events.push(evt); }, close: function (evt) { if (this.closed) throw new Error("Cannot close already closed MetaEvent"); this._closingEvent = evt; this.closed = true; }, init: function (stateMachine) { this._createEventIndex(); this._stateMachine = stateMachine; for (var id in this._eventIndex) { this._initEvent(this._eventIndex[id]); } }, dispose: function () { for (var id in this._eventIndex) { this._eventIndex[id].dispose(); } }, _initEvent: function (evt) { var _this = this; evt.init(); evt.on(function (evt) { if (this.id === _this._closingEvent.id && this.callback.apply(this.context || this.element, [evt]) !== false) { _this._stateMachine[_this.name](); } else if (this.id === _this._currentEvent.id) { this.callback.apply(this.context || this.element, [evt]); } else if (this.type !== _this._currentEvent.type && this.callback.apply(this.context || this.element, [evt]) !== false) { _this._disposePreviousEvents(this.id); _this._currentEvent = _this._eventIndex[this.id]; } }); }, _createEventIndex: function () { this._eventIndex = {}; for (var i = 0; i < this._events.length; ++i) { var evt = this._events[i]; this._eventIndex[evt.id] = evt; } this._eventIndex[this._closingEvent.id] = this._closingEvent; this._currentEvent = this._events[0] || this._closingEvent; }, _disposePreviousEvents: function (eventId) { for (var i = 0; i < this._events.length; ++i) { var evt = this._events[i]; if (evt.id !== eventId) { evt.dispose(); } else { break; } } } };
      
      







MetaEventは、BaseEventと同じ初期化と破棄と同様に、プッシュ経由で繰り返しイベントを追加し、閉じる経由で終了イベントを追加する可能性を意味します。 ここで、コールバックがfalseを返す場合、マシンはその状態を変更しないという事実に注意を払うことができます。 これはあまりきれいではありませんが、evt.preventDefaultを使用することも同様に悪いでしょう。 このコンテキストで少なくともfalseを返しても、デフォルトのイベントハンドラーとそのバブリングには影響しません。



実際、ステートマシン全体にねじ込むだけです。 そのため、 ここからオープンソースソリューションを使用しました



 var EventChain = function (element) { this._element = $(element); this._metaEvents = []; this._atLast = null; }; EventChain.prototype = { _lastEvent: function () { return this._metaEvents.length > 0 ? this._metaEvents[this._metaEvents.length - 1] : null; }, _createEventIndex: function () { this._eventIndex = {}; for (var i = 0; i < this._metaEvents.length; ++i) { var evt = this._metaEvents[i]; this._eventIndex[evt.id] = evt; } }, _createEvents: function () { return this._metaEvents.map(function (evt, index, metaEvents) { return { name: evt.name, from: evt.id, to: index + 1 < metaEvents.length ? metaEvents[index + 1].id : "atLast" } }); }, _createCallbacks: function () { var result = {}, _this = this; for (var i in this._eventIndex) { result["onenter" + this._eventIndex[i].id] = function (evt, from, to, data) { _this._eventIndex[to].init(this); } result["onleave" + this._eventIndex[i].id] = function (evt, from, to, data) { if (_this._eventIndex[from]) { _this._eventIndex[from].dispose(); } } } result["onatLast"] = function (evt, from, to) { if (_this._eventIndex[from]) { _this._eventIndex[from].dispose(); } if (_this._atLastCallback) { _this._atLastCallback.apply( _this._atLastContext || _this._element, arguments); } }; return result; }, // add event that will be handled only once once: function (type, element, callback, context) { if (element instanceof Function) { context = callback; callback = element; element = this._element; } var lastEvent = this._lastEvent(); if (lastEvent && !lastEvent.closed) { lastEvent.close(new BaseEvent(type, element, callback, context)); } else { var evt = new MetaEvent(); evt.close(new BaseEvent(type, element, callback, context)); this._metaEvents.push(evt); } return this; }, // add event that will be handled twice twice: function (type, element, callback, context) { return this .once(type, element, callback, context) .once(type, element, callback, context); }, // add event that will be repeated any times any: function (type, element, callback, context) { if (element instanceof Function) { context = callback; callback = element; element = this._element; } var lastEvent = this._lastEvent(); if (lastEvent && !lastEvent.closed) { lastEvent.push(new BaseEvent(type, element, callback, context)); } else { var evt = new MetaEvent(); evt.push(new BaseEvent(type, element, callback, context)); this._metaEvents.push(evt); } return this; }, // add event that will be repeated at least once onceAndMore: function (type, element, callback, context) { return this .once(type, element, callback, context) .any(type, element, callback, context); }, // set function that will be called after queue is done atLast: function (callback, context) { this._atLastCallback = callback; this._atLastContext = context; return this; }, // set event that will cancel queue immediately cancel: function (type, element, callback, context) { var _this = this; if (element instanceof Function) { context = callback; callback = element; element = this._element; } new BaseEvent(type, element, callback, context) .on("caught", function (evt) { if (this.callback.apply(this.context || this.element, [evt]) !== false) { _this.dispose(); } }) .init(); return this; }, // initialize state machine init: function () { this._createEventIndex(); var callbacks = this._createCallbacks(), events = this._createEvents(), stateMachine = StateMachine.create({ initial: this._metaEvents[0].id, final: "atLast", events: events, callbacks: callbacks }); return this; }, dispose: function () { for (var i = 0; i < this._metaEvents.length; ++i) { this._metaEvents[i].dispose(); } } };
      
      







MetaEventsからのチェーン自体は、最初は特定のDOM要素に合わせて調整され、jQueryの小さな拡張機能を介して渡されます。



 jQuery.fn.eventChain = function () { return new EventChain(this); };
      
      







描画ツールについては、ここですぐにあらゆる種類のパターンを投げましたが、これはプロトタイプでこれらのツールの多くがすでに必要だったためです。 コードが多すぎると、これが直線描画の見た目です。



 var LineDrawer = new (ConcreteDrawer.extend({ __type: "line", __draw: function (data) { return new SmartPath(data).draw(); }, __startDrawing: function (data) { return Board.EventLayer.eventChain() .once("mousedown", this._placeStartPoint, this) .any("mousemove", this.__drawTemporaryFigure, this) .once("mouseup", this._placeEndPoint, this) .cancel({"keydown": 27}, this.cancelDrawing, this) .atLast(this.__saveFigure, this) .init(); }, _placeStartPoint: function (evt) { this.__figureData.x1 = Board.EventLayer.pageX(evt); this.__figureData.y1 = Board.EventLayer.pageY(evt); }, __drawTemporaryFigure: function (evt) { this._placeEndPoint(evt); this.base(); }, _placeEndPoint: function (evt) { this.__figureData.x2 = Board.EventLayer.pageX(evt); this.__figureData.y2 = Board.EventLayer.pageY(evt); } }))();
      
      







実際、ご想像のとおり、LineDrawerは、たとえば、目的のクライアントイベントへの反応として、描画プロセスを開始できます(ツールバーのラインアイコンをクリックします)。 私はこれについて小さな責任の連鎖を書いたので、新しい描画ツールを作成するには数十行かかります。



プロトタイプの準備が完了した後、突然不気味な仮定に遭遇しました。顧客が単一のイベントではなく、パターン全体、イベントサブチェーンを繰り返したい場合です。 かなり些細なタスクだとしましょう:



ファンタスティックTK「ポリゴン」:

ユーザーとして、私は破線を描くことができるようにしたい。

1.左クリックすると、行の始まりがマークされます。

2.マウスの動きは中間結果を示します。

3.スペースを押すと、ポリラインの上部がマークされます。

4.ユーザーがマウスボタンを離すまで手順2と3を繰り返し、最後の中間結果を保存します。




上記で実装された概念では、そのようなタスクはすでに不可能です-少なくとも、任意の方法ではなく、特定の数のチェーンリンクのみを記述できます。

この内側の美しさはこのスタイルを必要としました:



 return Board.EventLayer.eventChain() .once("mousedown", this._placeStartPoint, this) .any(function (chain) { return chain .any("mousemove", this.__drawTemporaryFigure, this) .once({"keydown": 32}, this._placePolygonePoint, this); }, this) .once("mouseup", this._placePolygonePoint, this) .cancel({"keydown": 27}, this.cancelDrawing, this) .atLast(this.__saveFigure, this) .init();
      
      







このスタイルでは、既製のソリューションが既に隠れていたため、通常のBaseEventに少し複雑なCycleEventを追加する必要がありました。



 var CycleEvent = Base.extend({ constructor: function (cycle, element, context) { this._cycle = cycle; this._element = element; this._context = context; this.callback = function () {}; this.id = GuidFactory.create(); this.name = "me_" + this.id; this.type = "cycle_" + this.id; }, init: function () { this._cycleChain = this._cycle .apply(this._context || this, [this._element.eventChain()]) .atLast(this._restartCycle, this); this._cycleChain.init(); return this._cycleChain; }, dispose: function () { this._cycleChain.dispose(); }, _restartCycle: function () { this.dispose(); this.init(); this.trigger("caught"); } });
      
      







外部コントラクトはBaseEventと完全に同一であるため、このようなデータを処理できるように、EventQueueのメソッドに少しパッチを適用するだけで十分です。



 any: function (type, element, callback, context) { if (type instanceof Function) { return this._cycle(type, element) } else if (element instanceof Function) { context = callback; callback = element; element = this._element; } var lastEvent = this._lastEvent(); if (lastEvent && !lastEvent.closed) { lastEvent.push(new BaseEvent(type, element, callback, context)); } else { var evt = new MetaEvent(); evt.push(new BaseEvent(type, element, callback, context)); this._metaEvents.push(evt); } return this; }, // add cycle of events with same syntax _cycle: function (cycle, context) { var lastEvent = this._lastEvent(); if (lastEvent && !lastEvent.closed) { lastEvent.push(new CycleEvent(cycle, this._element, context)); } else { var evt = new MetaEvent(); evt.push(new CycleEvent(cycle, this._element, context)); this._metaEvents.push(evt); } return this; }
      
      







結果とFRPはどこで



ここでは、もちろん、このすべてにFRPが含まれているかどうかは議論の余地があります。 グラフィックプリミティブ上のデータセットをセットとして表す場合、実際には、eventChain()の後に記述するコードは、このセットに対する操作とその構成の記述です。 繰り返しイベントとイベントパターンを追加する機能により、これらすべてに柔軟性が追加されますが、一般に、一度のイベントで何らかのFRPを処理できます。

このコードの価値は、さらに議論の余地のある問題です。 しかし、タスクのコンテキストでは、彼は間違いなく彼の義務に完全に対処します。 もちろん、それを拡張することはたくさんあります。たとえば、プロミスのサポートを追加すると、複雑なアニメーションを美しく説明でき、同等のイベントの概念を追加すると(まだ実装されているため、異なるキーの押下を等しく監視できます)、シンプルなゲームを作成できます。



参照:


Cloud9のコード

Cloud9のデモ

ステートマシン

ラファエル.js



All Articles