The article is outdated, look for current information on the site
Warp9'a
There are many reactive and near-reactive libraries for creating a graphical interface on js: Angular, Knockout, React, RxJS ... One wonders why write another one. It turns out that in all of them, in addition to the fatal flaw, there are a few more.
')
Since RxJS and Bacon.js are libraries for event management, and do not provide primitives to facilitate the creation of interfaces, some questions simply do not apply to them. In principle, taking them as a basis for designing a GUI is a bit ridiculous, but I still included them in the table, as there is a big chance that someone will mention them in the comments.
Decoding Legends and Remarks
template basedAre templates a basic technique for building interfaces?
[1] in many tutorials and examples, the interface is really created only on the basis of templates, in spite of that AngularJS supports the creation of interfaces through custom directives.
controls composabilityDoes the library provide a mechanism for the consciousness of controls through the composition of existing (the principle of divide and conquer).
[2] In AngularJS and Knockout there are mechanisms for including templates (ng-include, template-binding), besides templates can be put into different files, but this does not follow the support of the composition of controls and modularity. The problem is that a complex JS application is not only a template, but also logic, and the logic in AngularJS is separated from templates (in templates, we must always remember about logic (face effect), but if so, divide and conquer the principle does not work, therefore, the composition does not give us advantages, therefore it is not.
Summary,
you can’t say that templates are linked, if logic is connected with them and it is separated from them. I’m not the only one who noticed this, the creators of React write:
“React doesn't use templates ... ... React is a library for building composable user interfaces.”
If you are still in doubt, think about why there are so few template engines for creating desktop, iOS or Android applications.
[3] If you create applications using only (!) Custom directives, then AngularJS supports both controls composability and modularization of controls.
modularization of controlsIs it possible to arrange controls so that they are self-sufficient and can be distributed independently of the application. Since there is only javascript (requirejs, commonjs) in the web of the distribution system, this question should be read as follows: can controls be wrapped in requirejs or commonjs.
[4] Only if AngularJS controls are issued as custom directives.
2 way bindingIs it possible to link two variables or a variable and a control so that when the variable changes, the second (or control) changes, and vice versa.
[5] In AngularJS, there is a possible binding between controls and variables, but not between variables and variables.
accidental memory leaksIs it easy to run into memory leaks in an application written using this library? It so happened that the answer “yes” also means that the code with leaks looks much simpler than the equivalent code without leaks, which means that if you use these libraries correctly, they lose some of their elegance.
I wrote a simple application on Knockout and ReactiveCoffee to demonstrate how easy it is to prevent leaks when using them, in fact, the
application on Knockout and the
application on ReactiveCoffee . And, of course, the control
application without leaks on Warp9 .
todomvcTodoMVC is a standard hello world for a variety of gui libraries on js, it is considered a good tone rule to have its own application implementation in the arsenal of examples.
[6] TodoMVC on ReactiveCoffee is only partially implemented: there is no tab switching (All, Active, Completed) and there is no work with local storage.
headlessIs it possible to use the library without reference to building interfaces, for example, on the server side for working with events.
pure jsWas it supposed by the library author that the main language that will use the library will be js.
[7] The creators of React propose to use the javascript dialect with support for embedded html-like markup (like xml support in scala) and compile it in js. It is possible to use pure js.
mixing markup and logicAre you supposed to mix markup and code to write an application?
It is believed that this is not very good, and therefore, it is necessary to separate them. But such an approach, while developing at least some meaningful applications, introduces more problems than it solves.
Behavior is described both by markup and by code, so logically they are inseparable: when changing the markup, we need to remember which code uses it, and vice versa, thus, by storing them separately, we create the side effect ourselves. If we still separate them, then with the growth of the application, we need funds both for composition / modularity (distribution) of the code, and for composition / modularity of markup; and the count of entities doubles.
There is another way to deal with complexity - we select independent behavior (markup + logic) in the control (abstraction) and, using the means of composition, we collect from the already existing controls all the more complex, up to our application. Drawing an analogy with the estimation of the complexity of algorithms, the division into logic and markup reduces the complexity by half (n -> n / 2), while the abstraction / composition reduces it from n to ln (n).
It's funny that if you look at the table again, it is clear that those libraries that encourage mixing provide composition and modularity.
[8] When developing guidelines in AngularJS, you are confronted with the need to mix logic and presentation.
simplicityWho knows the number of entities that the library introduces.
This is a very subjective opinion, but it seems to me that the number of entities in AngularJS is too large. React developers
share this opinion:
Number of concepts to learn
- React: 2 (everything is a component, some components have state). As your app grows, there's more to learn; just build more modular components.
- AngularJS: 6 (modules, controllers, directives, scopes, templates, linking functions). As your app grows, you'll need to learn more concepts.
rich reactive listVirtually all reactive libraries provide primitives for reactive variables, such as creating a new reactive variable associated with another reactive variable. In knockout, you can do it like this:
var a = ko.observable(0); var b = ko.compute(function() { return a() + 2 });
And in warp9 like this:
var a = new Cell(0); var b = a.lift(function(a) { return a+2; });
As for the lists, the support in Knockout and ReactiveCoffee is very scarce; in fact, they view the reactive list as a simple list contained in the reactive variable, which is updated when the list is updated. Thus, if you need to count the unit from the list and you want it to be reactive, then for each change in the list you need to recalculate it. In addition, if the list contains reactive values, then the logic of counting the aggregate becomes much more complicated and lies entirely on your shoulders.
Warp9 provides a richer interface for working with lists and uses clever data structures under the hood to recalculate values ​​as quickly as possible. The user can calculate the jet unit as follows:
var list = new List([new Cell(0), new Cell(1), new Cell(2)]); var sum = list.reduce(0, function(a,b) { return a + b; });
Now that we have seen that in many libraries there are indeed flaws and that it was not only the desire to entertain my CSW that caused me to create warp9, we now turn to the description of the library.
Warp9
At the heart of warp9 is a very simple idea: let's take the reactive model, apply the transformation to it and get the reactive DOM associated with the model. If the transformation is a simple js function, then we get the usual means of composition (composition of functions) and distribution (requirejs) out of the box.
An example of displaying a reactive model in a reactive DOM -
application (
source ):
Reactive primitives
Let's start with warp9 with reactive primitives, on the basis of which the GUI part of the library was created. In the presentation of a rather detailed description of them.
All examples from slides are available on
github .
Subscriptions and Leaks
The reactive model (observer pattern, publish-subscribe) is very convenient in some places, but you should not think that solving some problems it does not introduce others. One of the Achilles' heels of reactivity is memory leaks. If we go to the wiki to read about
memory leaks , among other things, we will see:
It is the responsibility of the driver to keep up references after use ...
This problem applies to many implementations of observer. She
is mentioned by Martin Fowler,
articles are devoted to her.
I believe that if in the application code that uses the library, errors of the same type occur systematically, then the problem is not the application code, but the library; the fact that these cases are described in the instructions for it is not an excuse for the library author. Therefore, the basic principle in the design of warp9 was the idea that the code that
can flow
should look suspicious or not work.
Let's see how this principle is implemented in practice. Take a look at the following code:
var cell = new Cell(0); var lifted = cell.lift(function(x) { return x+1; });
This code looks completely normal, so it’s natural to expect that the cell does not contain a reference to lifted and when all references to lifted are lost, the object will be collected by the garbage collector. In warp9 this is true, and now look at the knockout:
var cell = ko.observable(0); var lifted = ko.computed(function(){ return cell() + 1; });
Here the cell will contain a link to lifted, which is counterintuitive. Consider the code more difficult
var cell = new Cell(); var lifted = cell.lift(function(x) { return x+1; }); var dispose = lifted.onEvent(Cell.handler({ set: function(value) { console.info(“set: ” + value); }, unset: function() { console.info(“unset”); } })); cell.set(3); cell.set(5); cell.unset(); dispose(); cell.set(1);
It would be natural to expect from him.
unset > set: 4 > set: 6 > unset
But such “natural” behavior would require that the cell contain a reference to lifted, and this does not suit us, since it is a potential leak. Therefore, a choice was made - let better code that looks good does not work, than it works, but contains a leak; since it
is possible to detect inoperative code much faster than detecting a leak .
To make the code above work — you need to insert a “hint” to the reader that it can flow:
var cell = new Cell(); var lifted = cell.lift(function(x) { return x+1; }); var dispose = lifted.onEvent(Cell.handler({ set: function(value) { console.info(“set: ” + value); }, unset: function() { console.info(“unset”); } })); lifted.leak(); cell.set(3); cell.set(5); cell.unset(); dispose(); lifted.seal(); cell.set(1);
We added
lifted.leak()
and
lifted.seal()
, the code began to look suspicious and, in a magical way, it worked. Actually, calling a
leak
not magic, but activation of a reactive variable. When activated, you subscribe to your dependencies and activate them. Thus, by causing a leak for one object (a variable or a list), we transitively activate all its dependencies. After we have finished working with the object that has been activated by us, we must leave it in the state in which we got it - call the
seal
method.
Repeat the
leak
does not lead to re-activation, but only incriminates the internal counter. If the
leak
was called n times, then the
seal
must also be called n times; each
seal
call will decriminate an internal counter and when it reaches 0, actual deactivation will occur. This was done so that you do not think in what condition the object is transferred to you - call
leak
and
seal
pairs and everything will be fine.
You could already guess that cyclic dependencies are not allowed - we are limited to DAG.
Based on the example above, it was possible to guess that when subscribing to a variable, we can expect set and unset events, they are different from the list: data, add, remove
var list = new List(); list.leak(); list.onEvent(List.handler({ data: function(items) { // , items - // ; , // " , , " // items - { key: key, value: value } value - // , key - (id), // }, add: function(item) { // , , item // {key: key, value:value}, value - , key - }, remove: function(key) { // , key - } }));
Markup
Consider the classic hello world application - the user enters his name, for example, Denis and sees the reaction “Hello, Denis!”. In order to demonstrate more features, I also added a button that clears the input field.
The live application is available by
reference (its
source ).
From the example, it can be seen that the NAME function maps the model (reactive variable) to some description of the object model, which is then rendered inside the div with id "placeholder". The description itself is very similar to s-expression lisp, but instead of parentheses we use square ones, you can make jokes and name them z-expression (“z” refers to “s” as well as "[" refer to "(").
The grammar of z-expression is approximately as follows:
explicitChildren = '["' TAG_NAME '"' (',' ATTR)? (',' child) * ']'
implicitChildren = '["' TAG_NAME '"' (',' ATTR)? (reactive-list-of node) "]
child = node | '"' STRING '"' | Int |
(reactive-variable-of node) |
(reactive-variable-of STRING) |
(reactive-variable-of INT)
node = explicitChildren | implicitChildren
where TAG_NAME is the name of the “html” tag (in quotes because you can define your own tags), STRING is any string, INT is any integer, ATTR is an object describing tag attributes, css and events.
The properties of the ATTR object are mapped to the attributes of the constructed DOM element, the value can be reactive variables (except, of course, the id attribute - it must be constant). Another exception are properties starting with "!" - they must be functions and displayed on the element's events. The css properties also require special relationships, they can be written in two ways - as in the example above:
["input-text", { "css/background": name.isSet().when(false, "red") }, name]
or through the creation of a separate object:
["input-text", { "css": { "background": name.isSet().when(false, "red") } }, name]
In addition to these exceptions, there is another one - custom attributes and custom events; they contain ":" in the name. You can guess that they are doing something custom and by default they are not associated with attributes or events of the DOM element.
I hope now the example above has now become obvious. Let's complicate it: we wanted a camper — for a given list, to show as many hello worlds as there are elements in it, and use the values ​​from this list as default values. I used to say that warp9 uses composition as a basic technique when designing interfaces, so this is not a problem. Add new feature
function NAMES(names){ return ["div", names.lift(NAME)]; }
and replace
warp9.ui.renderer.render(placeholder, NAME(new Cell("Denis")));
on
var names = new List(); warp9.ui.renderer.render(placeholder, NAMES(names)); names.add(new Cell("Denis")); names.add(new Cell("Lisa"));
We saw how the principles of composition work, but beyond that, warp9 provides excellent support for modularity. However, when creating warp9, not a single line of code was written to realize this, in fact, the modularity of warp9 was opened after the library was written. The fact is that the representation in warp9 is described by the code, this implies automatic support for modularity available for js files; it turns out, we can easily use, for example, AMD for distributing individual controls. As an illustration, I rewrote the previous example with a list of names using AMD: the
application itself and its
sources .
Custom attributes
While the mechanism of custom attributes is not fully thought out and warp9 provides a few hard-hardened ones.
! key: enterAn element event corresponding to the “input-text” tag occurs when the user presses enter.
warp9: role and
! warp9: changedAttributes element corresponding to the tag "input-check". The warp9: changed event occurs when the checkbox state changes, the current value (true / false) is passed in the argument. If warp9: role is equal to “view”, then when the checkbox state changes, the element does not try to change the passed-in reactive variable (but the state itself changes after the variable).
! warp9: drawEach element has an event that is triggered when the element is drawn on the screen.
TodoMVC
If you understood everything that I wrote above, then you already have enough knowledge to understand the
sources of TodoMVC on warp9, by the way, a link to a live
application .
Conclusion
Now Warp9 has no version, it is a working concept, demonstrating:
- Compatibility as the basic principle of building UI
- Modularity through distribution through requirejs
- Lean on memory leaks
This set seems to me quite unique. It would be great to hear different opinions to bring this concept to mind.