プロキシなしのECMAScript-2015による双方向データバインディング

こんにちは、Habrの親愛なる読者。 この記事は、最近読んだ記事「ECMAScript-2015 Proxyを使用した一方向バインディングデータ」とは対照的です。 プロキシの形で不要な構造を持たない双方向の非同期バインディングを作成する方法の学習に興味がある場合は、catを要求します。



手紙を読むことに興味がない人のために、これらの手紙をすぐにクリックすることを勧めますDEMO binding



だから、その記事で私を混乱させ、自分で書くように動機付けたもの:



  1. 著者はデータバインディングについて話しますが、 observerの実装について説明します。 つまり オブジェクトのプロパティを変更するサブスクリプション。 もちろん、コールバックの助けを借りて、リンクを実装できますが、もっとシンプルなものが必要です。 バインディングという用語自体は、あるストレージユニットの値と別のストレージユニットの値との対応を簡単に示すものです。



  2. 非同期の実装はあまり成功していません-
    setTimeout(()=>リスナー(イベント)、0);
    最小タイムアウトで、すべてが正常であり、サブスクライバー関数は一定の最小間隔(4mcなど)で次々に呼び出されます。 しかし、たとえば500ミリ秒に増やす必要がある場合はどうでしょう。 次に、指定された間隔で単純に遅延が発生し、すべての関数が呼び出されますが、最小間隔もあります。 そして、私は、間隔、すなわち、加入者への呼び出しの間隔を示したいと思います。



  3. そしてもう少し-直接上書きによる共有ストレージフィールドの脆弱性には、DOM→JS、DOM→DOMバインディングの実装はありません。


さて、賢くならないで、あなたの「創造性」を示す時です。 問題のステートメントから始めましょう。



与えられた:





割り当て:





解決策:



主要な実装のアイデア:



  1. 共有リポジトリは作成されず、各オブジェクトは独自のバインディングを保存します。 そのようなオブジェクトの個別のクラスを作成しましょう。 このクラスのコンストラクタは、新しいオブジェクトを作成するか、必要な構造を持つ既存のオブジェクトを展開します。



  2. オブジェクトプロパティへのアクセスをインターセプトするために機能が必要ですが、 プロキシオブジェクトの形で不要な構造を作成することはありません。 このため、ECMAScript 5.1で定義されているゲッターとセッター( Object.defineProperty )の機能は完璧です。



  3. 順次非同期バインディングの場合、キューを実装します。 このためにsetTimeout + Promisesを使用してみましょう。


実装:



  1. バインダークラスの構造:

    画像

    1-バインダークラス図



    静的:



    • hash-関数のハッシュを計算し、オブザーバー関数を識別するために使用されます
    • 遅延 -非同期キューのタイムアウト
    • queue-非同期キューを作成します
    • タイムアウト -_timeoutのゲッター/セッター
    • _timeout-デフォルトのタイムアウト
    • prototypes- 「拡張」オブジェクトのプロトタイプを保存します
    • createProto- 「拡張」オブジェクトのプロトタイプを作成します


    インスタンス:



    • 変換されたプロパティ」 -ゲッター/セッターに変換されたオブジェクトのプロパティ
    • _binder-サービス情報
    • エミッタ -現在バインディングを開始しているオブジェクトを示します
    • バインディング - バインディングストア
    • ウォッチャー -オブザーバーのリポジトリ
    • プロパティ - プロパティのストレージ-ゲッター/セッターに変換された値
    • _bind / _unbind-バインディング/アンバインディングの実装
    • _watch / _unwatch-サブスクリプション/視覚実装


  2. クラスコンストラクター:



    constructor(obj){ let instance; /*1*/ if (obj) { instance = Binder.createProto(obj, this); } else { instance = this; } /*2*/ Object.defineProperty(instance, '_binder', { configurable: true, value: {} }); /*3*/ Object.defineProperties(instance._binder, { 'properties': { configurable: true, value: {} }, 'bindings':{ configurable: true, value: {} }, 'watchers':{ configurable: true, value: {} }, 'emitter':{ configurable: true, writable: true, value: {} } }); return instance; } /*1*/ — ,      ,    .       .   `createProto` .  /*8*/ /*2*//*3*/ —   - `_bunder`,      ,     ,   .  «emitter»     ,     .     ,       (`writable: false`).
          
          





  3. 静的クラスメソッド:



     /*    */ /*4*/ static get timeout(){ return Binder._timeout || 0; } /*5*/ static set timeout(ms){ Object.defineProperty(this, '_timeout', { configurable: true, value: ms }); } /*6*/ static delay(ms = Binder.timeout){ return new Promise((res, rej) => { if(ms > 0){ setTimeout(res, ms); } else { res(); } }); } /*7*/ static get queue(){ return Promise.resolve(); } /*4*/-/*5*/`_timeout`       .  ,       ,   ES6        -. /*6*/-/*7*/  queue    ,     .  `delay`  ,   ""        .       .
          
          







     /*   */ /*8*/ static createProto(obj, instance){ let className = obj.constructor.name; if(!this.prototypes){ Object.defineProperty(this, 'prototypes', { configurable: true, value: new Map() }); } if(!this.prototypes.has(className)){ let descriptor = { 'constructor': { configurable: true, value: obj.constructor } }; Object.getOwnPropertyNames(instance.__proto__).forEach( ( prop ) => { if(prop !== 'constructor'){ descriptor[prop] = { configurable: true, value: instance[prop] }; } } ); this.prototypes.set( className, Object.create(obj.__proto__, descriptor) ); } obj.__proto__ = this.prototypes.get(className); return obj; } /*8*/—    .        .      ,         - `Binder.prototypes`
          
          





     /*   */ /*9*/ static transform(obj, prop){ let descriptor, nativeSet; let newGet = function(){ return this._binder.properties[prop];}; let newSet = function(value){ /*10*/ let queues = [Binder.queue, Binder.queue]; /*11*/ if(this._binder.properties[prop] === value){ return; } Object.defineProperty(this._binder.properties, prop, { configurable: true, value: value }); if(this._binder.bindings[prop]){ this._binder.bindings[prop].forEach(( [prop, ms], boundObj ) => { /*12*/ if(boundObj === this._binder.emitter) { this._binder.emitter = null; return; } if(boundObj[prop] === value) return; /*13*/ queues[0] = queues[0] .then(() => Binder.delay(ms) ) .then(() => { boundObj._binder.emitter = obj; boundObj[prop] = value; }); }); queues[0] = queues[0].catch(err => console.log(err) ); } /*14*/ if( this._binder.watchers[prop] ){ this._binder.watchers[prop].forEach( ( [cb, ms] ) => { queues[1] = queues[1] .then(() => Binder.delay(ms) ) .then(() => { cb(value); }); }); } if( this._binder.watchers['*'] ){ this._binder.watchers['*'].forEach( ( [cb, ms] ) => { queues[1] = queues[1] .then(() => Binder.delay(ms) ) .then(() => { cb(value); }); }); } queues[1] = queues[1].catch(err => console.log(err)); }; /*15*/ if(obj.constructor.name.indexOf('HTML') === -1){ descriptor = { configurable: true, enumerable: true, get: newGet, set: newSet }; } else { /*16*/ if('value' in obj) { descriptor = Object.getOwnPropertyDescriptor( obj.constructor.prototype, 'value' ); obj.addEventListener('keydown', function(evob){ if(evob.key.length === 1){ newSet.call(this, this.value + evob.key); } else { Binder.queue.then(() => { newSet.call(this, this.value); }); } }); } else { descriptor = Object.getOwnPropertyDescriptor( Node.prototype, 'textContent' ); } /*17*/ nativeSet = descriptor.set; descriptor.set = function(value){ nativeSet.call(this, value); newSet.call(this, value); }; } Object.defineProperty(obj._binder.properties, prop, { configurable: true, value: obj[prop] }); Object.defineProperty(obj, prop, descriptor); return obj; } /*9*/ -  `transform`   .   JS ,      `obj._binder.properties`,     /.    DOM ,      /. /*10*/ -        . /*11*/ -                . /*12*/ -      -      .         `obj._binder.emitter`  .          .        . /*13*/ -         . /*14*/ -          . /*15*/ -      DOM /*16*/ -    DOM .     "" `input`, `textarea`   `value`    `textContent`.  ""  / `value`    (. `. 2`). ,  `input`   `HTMLInputElementPrototype`. `textContent`   /    `Node.prototype`(. `. 3`).    /   `Object.getOwnPropertyDescriptor`.     ""      . /*17*/ -     ,      . /**/ -  `newSet`  `newGet`, ,     .
          
          





    画像






    図2-「value」プロパティの継承



    画像






    図3-`div`要素の例を使用した` textContent`プロパティの継承



    わかりやすくするために、DOM要素の変換を説明する別の画像を示します。例としてdiv要素を使用します( 図4



    画像






    図4-DOM要素の変換の図。



    非同期キューについて説明します。 最初は、特定のプロパティのすべてのバインディングに対して1つの実行キューを作成することを想定していましたが、不快な効果が生じました。図を参照してください 5 。 つまり 最初のバインディングは、キュー全体が完了するのを待ってから、値を再度更新します。 個別のキューの場合、最初のバインディングが特定の間隔で更新され、後続のすべてが前のバインディングの間隔の合計を通じて更新されることは確かです。



    画像






    図5一般的な実行キューと個別の実行キューの比較。



  4. バインダークラスのインスタンスメソッド:



      /*18*/ _bind(ownProp, obj, objProp, ms){ if(!this._binder.bindings[ownProp]) { this._binder.bindings[ownProp] = new Map(); Binder.transform(this, ownProp); } if(this._binder.bindings[ownProp].has(obj)){ return !!console.log('Binding for this object is already set'); } this._binder.bindings[ownProp].set(obj, [objProp, ms]); if( !obj._binder.bindings[objProp] || !obj._binder.bindings[objProp].has(this)) { obj._bind(objProp, this, ownProp, ms); } return this; } /*19*/ _unbind(ownProp, obj, objProp){ try{ this._binder.bindings[ownProp].delete(obj); obj._binder.bindings[objProp].delete(this); return this; } catch(e) { return !!console.log(e); } }; /*20*/ _watch(prop = '*', cb, ms){ var cbHash = Binder.hash(cb.toString().replace(/\s/g,'')); if(!this._binder.watchers[prop]) { this._binder.watchers[prop] = new Map(); if(prop === '*'){ Object.keys(this).forEach( item => { Binder.transform(this, item); }); } else { Binder.transform(this, prop); } } if(this._binder.watchers[prop].has(cbHash)) { return !!console.log('Watchers is already set'); } this._binder.watchers[prop].set(cbHash, [cb, ms]); return cbHash; }; /*21*/ _unwatch(prop = '*', cbHash = 0){ try{ this._binder.watchers[prop].delete(cbHash); return this; } catch(e){ return !!console.log(e); } }; /*18*/ - /*19*/ -  /.          ,   ,   ,       .           () . . `. 6` /*20*/ - /*21*/ -  /.           (   - "*").          .        .
          
          





    画像






    図6ネコバインダーに会う





要約:



良い:


  1. 直接書き換えからのプロパティの保護。
  2. オブジェクトのプロパティを変更するサブスクリプションのメカニズム。
  3. バインディングとサブスクリプションのカスタマイズ可能な非同期キュー。
  4. 相互の「正直な」バインディング、すなわち ある意味と別の意味の対応を示すだけです。


悪い:


  1. DOM要素の場合 、プロパティ'value'および'textContent'のみにバインドします。
  2. 2つのオブジェクト間に1つのバインディングのみを指定する機能。


PSこれは決してすぐに使えるソリューションではありません。 これはいくつかの考えの単なる実装です。



ご清聴ありがとうございました。 コメントと批判を歓迎します。

それだけです! 最後に:)



All Articles