📜 ⬆️ ⬇️

One-way data binding with ECMAScript-2015 Proxy



Good day, habravchane. Today we will create a data warehouse with the function of one-way data binding using Proxy and some other ECMAScript 2015 buns.

What is a proxy?


Simply put, a proxy is a wrapper object that allows you to intercept a call to the object on the basis of which it was created. To intercept appeals, a proxy armed with an arsenal of traps has several functions of interceptors. Full information about the list of interceptors and all methods of proxy can be found here .
')

What do we do?


We implement object storage with change tracking functionality using a proxy, i.e. some semblance of the late Oo with some extra buns.

So let's go ...

For work


We imply that our repository is an instance of a certain storage factory class.

"use strict"; class OS { //    } window.ObserveStorage = new OS(); 

Everything that happens in the future will occur within the OS class.

Our repository should have the following data structure:

 {  : {  :    }  :{  :{   :{ id :  } } } } 

Accordingly, in order to implement all the necessary functionality, we define in the designer objects for storing data:

 class OS { constructor() { this.storage = new Map(); //   this.listeners = new Map(); //   this.serviceField = new Set([`on`, 'un']); //”” , ..    . } } 

I will intentionally omit the description of the Map and Set classes. In case you want to know about them in detail, then you are here and here .

The serviceField field is necessary in order to exclude the possibility of rewriting or busting service ones, but more on that later.

The next step is to organize adding a new object to our repository.

Having an object:

 let object = {key1: ”data”, key2: 1} 

implement the following method of adding an object to the repository:

 let wrapper = ObserveStorage.add(key, object); //return Proxy 


The first parameter we will define the key under which the object will be recorded in the storage, and the second - the object itself. At the output we get the proxy wrapper of the base object.

In advance, we mean that not all users will be interested in the functionality of receiving an object from the storage, and monitoring all keys is not always convenient, so this entry will be valid as well:

 let wrapper = ObserveStorage.add(object); 

Since we use different keys and id, we make a simple method for generating them.

 static __getId() { return (`${Math.random().toFixed(10).toString().replace("0.", "")}${Date.now()}`) } 

Now that we have defined the interface and have a tool for generating various kinds of identifiers, we can start developing the method.

 add(...arg) { //    1  2  let key, object; if(arg.length == 1){ [object] = arg; key = OS.__getId(); //  id } else [key, object] = arg; //       ,  ,    ,  . //      ,     . // 1)     : if (this.storage.has(key)) { throw new Error(`key ${key} is already in use`); } // 2)     () ,     ,     (:  ,    ,      –      ): let self = this; object.on = (...arg)=> self.on(key, ...arg); //  object.un = (...arg)=> self.un(key, ...arg); //  //        storage const proxy = this.getProxy(key, object); //return Proxy //    Map  this.listeners.set(key, new Map()); //, ,     this.storage.set(key, proxy); // ,     ,     return proxy; } 

The getProxy method remained undisclosed , I don’t know about you, but I hate secrets. Therefore, let's go:

 // getProxy  2 , 1 –  ,    ,  2 –   . getProxy(key, object){ let self = this; //    ,      return new Proxy(object, { //       get(target, field) { return target[field]; }, //       set(target, field, value) { // ,       if(self.serviceField.has(field)) throw new Error(`Field ${field} is blocked for DB object`); const oldValue = target[field]; target[field] = value; //       fire  OS. self.fire(key, { type: oldValue ? "change" : "add", property: field, oldValue: oldValue, value: value, object: self.get(key) }); //       , ,           Oo,    -  . return true }, //     deleteProperty(target, field) { //       if (!field in target || self.serviceField.has(field)) { return false; } const oldValue = target[field]; delete target[field]; self.fire(key, { type: "delete", property: field, oldValue: oldValue, value: undefined, object: self.get(key) }); return true; }, //  Object.getOwnPropertyNames() ,         “”      ownKeys(target) { let props = Object.keys(target) .filter(function (prop) { return !(self.serviceField.has(prop)); }); return props; } } ); } 

It is important to note that when generating an event

 self.fire(key, { type: oldValue ? "change" : "add", property: field, oldValue: oldValue, value: value, object: self.get(key) }); 

The object is not the target , but the wrapper. This is necessary so that the user, while changing the object in the callback , does not make any untraceable changes. Initially, I passed a copy of the object there, which, in fact, is also not very good. In the block above, methods such as .get and .fite were lit up, so, following in order, let's talk about them.

The .get method only checks for the presence of an object in the repository and returns it.

 get(key) { if(this.storage.has(key)) return this.storage.get(key); else{ console.warn(`Element ${key} is not exist`); return undefined; } } 

Before talking about the .fire method, it’s worth mentioning the event subscription. The subscription uses the following interface:

 wrapper.on(callback, property = "*"); 

Where

 property = "*" 

is the default value and indicates a subscription to all fields of this object.

For example:

 wrapper.on(event => console.log(JSON.stringify(event)), "value"); wrapper.data = "test"; //   wrapper.value = 2; // Object{"type":"change","property":"value","oldValue":4,"value":2,"object":{"data":"test","value":2}} wrapper.on(event => console.log(JSON.stringify(event)), "*"); wrapper.data = "test"; // Object{"type":"change","property":"data","oldValue":”text”,"value":”test”,"object":{"data":"test","value":1}} 

We integrate this method into the object at the time the object is written to the storage (see above). The method itself is the following function:

 on(key, callback, property = "*") { //   callback   if (!key || !callback) { throw new Error("Key or callback is empty or not exist"); } // Map     const listeners = this.listeners.get(key), //  id    subscriptionId = OS.__getId(); //  ,     ,    ,      Map !listeners.has(property) && listeners.set(property, new Map()); //        id listeners .get(property) .set(subscriptionId, callback); //  id     return subscriptionId; } 

We pay special attention to the first parameter of the .on method. Attentive ones noticed that 1 or 2 parameters are passed, but the method expects 3, one of which is the key.

And especially attentive ones remember that we locked the key in the method at the moment of initialization of the object in the repository, namely in the line:

 object.on = (...arg)=> self.on(key, ...arg); 

To unsubscribe, you must use the subscription Id received during the subscription.

 wrapper.un(subscriptionId); 

Function Description:

 un(key, subscriptionId) { //    ,   if (!key) { throw new Error("Key is empty or not exist"); } //     const listeners = this.listeners.get(key); if (listeners) //      Map for (let listener of listeners.values()) { //        if (listener.delete(subscriptionId)) return true; } return false; } 

I like the use of id for various kinds of operations, since it allows you to clearly identify user actions in a fairly transparent form.

And so, we got to the method call .fire, which pulls all the callback hung on the wrapper:

 fire(key, event) { //   let listeners = this.listeners.get(key) ,property = event.property; //    listeners.has(property) && this.fireListeners(event, listeners.get(property)); //    listeners.has("*") && this.fireListeners(event, listeners.get("*")); } 

The fireListeners method is transparent and does not need to be explained:

 fireListeners(event, listeners) { listeners.forEach((listener)=> { setTimeout(()=> listener(event), 0); }) } 

Summing up


Thus, we wrote our data warehouse for only 150 lines of code, while still being able to subscribe to object changes. Following the legacy of Oo, we do not currently wrap nested objects and do not process arrays, but all this can be done with the proper desire.

The full code can be found here .

Has been with you, sinires .
Good to you, earthlings.

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


All Articles