📜 ⬆️ ⬇️

Bilateral data binding with ECMAScript-2015 without proxy

Hello, dear readers of Habr. This article is a kind of opposition to the article “One-way data binding with ECMAScript-2015 Proxy”, which I recently read. If it is interesting to you to learn how to make a bilateral asynchronous binding without unnecessary structures in the form of a Proxy, then I ask for cat.

Those who are not interested in reading the letters, I invite you to immediately push on these very letters DEMO binding

So, what embarrassed me in that article and motivated me to write my own:

  1. The author talks about data binding, but describes the implementation of observer a. Those. Subscription to change the properties of objects. Of course, with the help of callbacks, you can implement the binding, but somehow you want something simpler. The term binding itself implies a simple indication of the correspondence of the value of one storage unit to the value of another.
    ')
  2. Not very successful implementation of asynchrony -
    setTimeout (() => listener (event), 0);
    With a minimum timeout, everything is fine, the functions of the subscribers are called one after another through a constant minimum interval (sort of like 4mc). But what if we need to increase it, for example, to 500 ms. Then there will simply be a delay for a given interval and then all functions will be called, but also with a minimum interval. And I would like to specify the interval, namely, between calls to subscribers.

  3. And a little more - the insecurity of the shared storage fields from direct rewriting; there is no implementation of the DOM → JS bindings, DOM → DOM bindings.

Well, enough to be clever, it's time to show your "creativity". Let's start with the formulation of the problem.

Given:


The task:


Decision:

Basic ideas for implementation:

  1. No shared storage will be created; each object will store its bindings itself. Create a separate class of such objects. The constructor of this class will create new objects or expand existing objects with the structures we need.

  2. The necessary functionality is to intercept access to the properties of an object, but without creating unnecessary structures in the form of proxy objects. For this, the getters and setters functionality ( Object.defineProperty ) defined in ECMAScript 5.1 is perfect.

  3. For sequential asynchronous binding, we implement queues. Let's try using setTimeout + Promises for this.

Implementation:

  1. Binder class structure:
    image
    Fig. 1 - class Binder diagram

    Static:

    • hash - calculates the hash function, will be used to identify the functions of observers
    • delay - timeout in the asynchronous queue
    • queue - creates an asynchronous queue
    • timeout - getter / setter for _timeout
    • _timeout - default timeout
    • prototypes - stores prototypes for "extended" objects
    • createProto - creates prototypes for "extended" objects

    Instance:

    • Transformed properties” - properties of objects converted to getter / setter
    • _binder - service information
    • emitter - points to the object that is currently initiating the binding
    • bindings - storage bindings
    • watchers - the repository of observers
    • properties - property storage - values ​​that were converted to getter / setter
    • _bind / _unbind - the implementation of binding / decoupling
    • _watch / _unwatch - implementation of subscription / optical

  2. Constructor class:

    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. Class static methods:

     /*    */ /*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`, ,     . 

    image

    Fig.2 - inheritance of the property `value`

    image

    Fig.3 - inheritance of the `textContent` property, by the example of the` div`

    For clarity, I will give another image explaining the transformation of the DOM element, for example, the element “div” ( Fig. 4 )

    image

    Figure 4 - scheme of the transformation of the DOM element.

    Now about asynchronous queues. In the beginning, I intended to make one execution queue for all the bindings of a specific property, but then an unpleasant effect appeared, see fig. 5 Those. the first binding will wait for the entire queue to be executed before updating the value again. In the case of separate queues, we know for sure that the first binding will be updated at the specified interval, and all subsequent ones will be updated through the sum of the intervals of the previous binding.

    image

    Fig.5 Comparison of the common execution queue with separate ones

  4. Binder class instance methods:

      /*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*/ -  /.           (   - "*").          .        . 

    image

    Fig.6 Meet the cat Binder


Results:

Good:

  1. Protection of properties from direct rewriting;
  2. The mechanism of subscriptions to change the properties of the object;
  3. Customizable asynchronous bind and subscription queues;
  4. Bilateral "honest" binding, i.e. we simply indicate the correspondence of the value of one to another.

Poorly :

  1. For DOM elements, binding only to the 'value' and 'textContent' properties ;
  2. Ability to specify only one binding between two objects;

PS This is by no means a ready-to-use solution. This is just the realization of some thinking.

Thank you all for your attention. Comments and criticism are welcome.
Everything! Finally the end :)

Source: https://habr.com/ru/post/315410/


All Articles