Developing your own framework and professional growth of a JS programmer

Have you ever wondered how frameworks work? The author of the material, the translation of which we are publishing today, says that when he, many years ago, after studying jQuery , came across Angular.js , what he saw seemed to him very complicated and incomprehensible. Then Vue.js appeared, and while working with this framework, he was inspired to write his own two-way data binding system . Such experiments contribute to the professional growth of the programmer. This article is intended for those who want to expand their own knowledge in the field of technology on which modern JS frameworks are based. In particular, the discussion here will deal with how to write the core of your own framework that supports custom attributes of HTML elements, reactivity, and two-way data binding.

image

About reactivity system


It will be good if we, at the very beginning, deal with how the reactivity systems of modern frameworks work. In fact, everything is quite simple. For example, Vue.js, when declaring a new component, proxies each property (getters and setters) using the proxy design pattern.

As a result, the framework will be able to detect every fact of a value change, executed from both the code and the user interface.

Proxy pattern


The proxy pattern is based on the overloading of access mechanisms to an object. This is similar to how people work with their bank accounts.

For example, no one can work directly with his account, changing its balance in accordance with their needs. In order to make some actions with the account, you need to contact someone who has permission to work with him, that is - to the bank. The bank plays the role of a proxy between the account holder and the account itself.

var account = {    balance: 5000 } var bank = new Proxy(account, {    get: function (target, prop) {        return 9000000;    } }); console.log(account.balance); // 5,000 ( ) console.log(bank.balance);    // 9,000,000 (   ) console.log(bank.currency);   // 9,000,000 (  ) 

In this example, using the bank object to access the account balance represented by the account object, the getter function is overloaded, which results in the result of such a query always returning 9,000,000, instead of the actual value of the property, even if this property does not exist.

Here is an example of a setter function overload.

 var bank = new Proxy(account, {   set: function (target, prop, value) {       //      0       return Reflect.set(target, prop, 0);   } }); account.balance = 5800; console.log(account.balance); // 5,800 bank.balance = 5400; console.log(account.balance); // 0 (  ) 

Here, by overloading the set function, you can control its behavior. For example, you can change the value that you want to write to a property, write data to some other property, or even do nothing.

Reactivity System Example


Now that we’ve dealt with the proxy pattern, let's start developing our own JS framework.

In order not to complicate it, we will use a syntax that is very similar to that used in Angular.js. As a result, the declaration of the controller and the binding of template elements to the properties of the controller will look simple and clear.

Here is the template code.

 <div ng-controller="InputController">   <!-- "Hello World!" -->   <input ng-bind="message"/>     <input ng-bind="message"/> </div> <script type="javascript"> function InputController () {     this.message = 'Hello World!'; } angular.controller('InputController', InputController); </script> 

First you need to declare a controller with properties. Next - use this controller in the template, and finally - use the ng-bind attribute in order to establish two-way data binding for the value of the element.

Parsing a template and instantiating a controller


In order for us to have certain properties to which we can bind data, we need a place (controller) in which we can declare these properties. Thus, it is necessary to describe the controller and include it in the framework.

In the process of working with controllers, the framework will look for elements that have ng-controller attributes. If what was found, will correspond to one of the declared controllers, the framework will create a new instance of this controller. This controller instance is only responsible for this particular template fragment.

 var controllers = {}; var addController = function (name, constructor) {   //     controllers[name] = {       factory: constructor,       instances: []   };     // ,     var element = document.querySelector('[ng-controller=' + name + ']');   if (!element){      return; //  ,      }     //         var ctrl = new controllers[name].factory;   controllers[name].instances.push(ctrl);     //   ..... }; addController('InputController', InputController); 

Below is the declaration of a “homemade” variable controllers . Note that the controllers object contains all the controllers declared in the framework by calling addController .

 var controllers = {   InputController: {       factory: function InputController(){           this.message = "Hello World!";       },       instances: [           {message: "Hello World"}       ]   } }; 

Each controller has a factory function, this is done so that, if necessary, you can create an instance of a new controller. In addition, the framework stores, in the instances property, all controller instances of the same type used in the template.

Search for items involved in data binding


At the moment we have a controller instance and a template fragment using this controller. Our next step is to search for elements with data that need to bind to the properties of the controller.

 var bindings = {}; //  : element   DOM,   Array.prototype.slice.call(element.querySelectorAll('[ng-bind]'))   .map(function (element) {       var boundValue = element.getAttribute('ng-bind');       if(!bindings[boundValue]) {           bindings[boundValue] = {               boundValue: boundValue,               elements: []           }       }       bindings[boundValue].elements.push(element);   }); 

This shows how to store all object bindings using a hash table . The variable in question contains all the properties for the binding, with their current values, and all DOM elements bound to a specific property.

This is what our version of the bindings variable looks like:

     var bindings = {       message: {           //   :           // controllers.InputController.instances[0].message           boundValue: 'Hello World',           // HTML- (   ng-bind="message")           elements: [               Object { ... },               Object { ... }           ]       }   }; 

Two-way binding of controller properties


After the framework has completed preliminary training, it is time for one interesting case: two-way data binding. The meaning of this is as follows. First, the properties of the controller need to be tied to the elements of the DOM, which will allow updating them when the values ​​of the properties change from the code, and secondly, the elements of the DOM must also be tied to the properties of the controller. Due to this, when the user acts on such elements, this leads to a change in the properties of the controller. And if several HTML elements are attached to the property, this also leads to the fact that their state is also updated.

Detection of changes made from code using a proxy


As mentioned above, Vue.js wraps the components in a proxy in order to detect changes in properties. Do the same by proxying only the setter for the controller properties involved in the data binding:

 //  : ctrl -    var proxy = new Proxy(ctrl, {   set: function (target, prop, value) {       var bind = bindings[prop];       if(bind) {           //    DOM,               bind.elements.forEach(function (element) {               element.value = value;               element.setAttribute('value', value);           });       }       return Reflect.set(target, prop, value);   } }); 

As a result, when writing a value to an anchored property, the proxy will detect all the elements associated with this property and pass them a new value.

In this example, we support only the binding of input elements, since here only the value attribute is set.

Reaction to element events


Now we just have to ensure the response of the system to user actions. To do this, take into account the fact that DOM elements trigger events when changes in their values ​​are detected.

Let's organize listening of these events and record in the properties of the new data attached to elements received from events. All other elements attached to the same property will be, thanks to the proxy, automatically updated.

 Object.keys(bindings).forEach(function (boundValue) {   var bind = bindings[boundValue];     //          bind.elements.forEach(function (element) {     element.addEventListener('input', function (event) {       proxy[bind.boundValue] = event.target.value; //  ,          });   }) }); 

Results


As a result, by putting together all the things we were discussing here, you will get your own system that implements the basic mechanisms of modern JS frameworks. Here is a working example of the implementation of such a system. We hope this material will help everyone to better understand how modern web development tools work.

Dear readers! If you professionally use modern JS frameworks, please tell us about how you started learning them and what helped you to understand them.

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


All Articles