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.

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);
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"> <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.
