There is a wonderful site
http://todomvc.com/ , which demonstrates the solution of the same task using different JavaScript MV * (
Model-View- [Controller] ) frameworks. Now there are dozens of different frameworks, each of which has its own advantages and disadvantages. There are such giants as
Angular ,
Ember and
Backbone . Despite the high competition, I would still like to demonstrate my MV * framework -
jWidget .
I quickly looked through all the solutions presented on the TodoMVC website and did not find any framework similar to jWidget. The fact is that, in addition to JavaScript, I program a lot in object-oriented programming languages ​​such as Java, C #, and in the past in C ++. Therefore, I am a big fan of object-oriented programming,
SOLID principles, and
object-oriented design patterns . I do not need a framework that would hamper my ability to use standard object-oriented solutions. What I saw in existing TodoMVC solutions does not inspire confidence in this regard. As a rule, they provide some declarative syntax and a powerful template engine, but the object-oriented basis of all this, even if it exists, is hidden from our eyes.
Documentation in English: http://enepomnyaschih.imtqy.com/jwidget/index.html#!/guide/home')
Documentation in Russian: http://enepomnyaschih.imtqy.com/jwidget/index.html#!/guide/ruhomeProject on GitHub: https://github.com/enepomnyaschih/jwidgetTwitter: @jwidgetprojectImplement TodoMVC on jWidget: http://enepomnyaschih.imtqy.com/todomvc/labs/architecture-examples/jwidget/release/The Source link does not work there right now, because jWidget is only in my fork. Below is the correct link to the source code.
TodoMVC source code on jWidget: https://github.com/enepomnyaschih/todomvc/tree/gh-pages/labs/architecture-examples/jwidget/Briefly list the main characteristics of jWidget:
1. Strict compliance with the principles of the PLO. A fully documented bilingual class library with examples of tutorials.
2. The speed of the script is above all. Hence the explicit declaration of the class constructor and the minimal use of closures when declaring classes, since In Google Chrome, inheritance through prototypes is
much more efficient than inheritance using the Module pattern (in Firefox, on the contrary, but there the difference is not so great).
3. No manipulation in the model requires a complete re-rendering of the view. Each component is rendered only once, after which it only updates its individual elements, thereby ensuring a high speed of the application.
4. The framework runs on
jQuery .
5. It has the simplest template engine that does not require preprocessing before being sent to the function
https://api.jquery.com/jQuery.parseHTML/ . No magic in the templates, no inline-code: all Data binding is done in the component's JavaScript code. Due to this, the same Data binding techniques can be used both for linking a view with a model, and for connecting objects within a model or within a view, which is often helpful.
6. All objects after use are completely destroyed. This ensures an economical consumption of client resources and the absence of unforeseen errors in the console from "dead" objects trying to handle some event. For example, you can use the same model for the duration of the application, on the fly changing its presentation. Any view listens to model events, but after the view is removed from the DOM, it must unsubscribe from these events so as not to waste processor time processing these events and so that the garbage collector can clear the memory. There is an easy way to destroy objects - the so-called. object aggregation mechanism.
7. Own application
builder -
jWidget SDK - simplifies application development and performs code optimization before release to production. It is planned to replace it with a stack of plugins for
GruntJS . Just when I started developing jWidget, GruntJS or something like that did not exist.
To confirm that jWidget is very fast, I measured the time to add 500 entries to TodoMVC with a wait of 0 milliseconds after adding each entry to give the browser time to redraw the view. Also, I roughly measured the time of the Select all and Clear completed operations for 500 entries. The results are as follows:
- Angular JS - 16847 milliseconds. The Select all and Clear completed operations are performed instantly.
- Angular JS (performance optimized version) - 13287 milliseconds. The Select all and Clear completed operations are performed instantly.
- Ember JS - 13095 milliseconds. The Select all and Clear completed operations take about 3 seconds.
- Backbone - 9506 milliseconds. The Select all and Clear completed operations take about 3 seconds.
- jWidget - 9974 milliseconds. The Select all and Clear completed operations are performed instantly.
- YUI - more than a minute. Not wait.
As you can see, only Backbone slightly surpassed jWidget in the speed of adding records, but it was far behind the speed of Select all and Clear completed. At the same time, note that the lag of Angular and Ember in 3 seconds is actually significant, since in all cases a lot of time was consumed by a 500-fold call to setTimeout. In general, of the 3 most popular frameworks, not one managed to cope with large amounts of data, while jWidget showed itself at the height.
Now I’ll tell you about the jWidget operation mechanism. The framework consists of 5 layers:
- Classes and objects . Class inheritance The mechanism of aggregation of objects.
- Events Announcement of events. Subscribe, unsubscribe and generate events.
- Properties and their helpers . Creating new properties based on existing ones. Data binding based on properties.
- Collections and their synchronizers . Array, dictionary, set. Creating new collections based on existing ones. Data binding based on collections.
- Components . Templates Connection of template elements with the component code. Creating child components using Data binding based on properties and collections.
1. Classes and objects.
Below is an example of a jWidget class declaration. The class is created by standard inheritance through the prototype, diluted with a small amount of syntactic sugar.
One of the key features of
JW.Class is an object aggregation mechanism that serves to destroy objects that are under the control of another object. I drew this idea from the introduction to the book
Object-Oriented Design. Design patterns from the "gang of four." It says that all pointers to objects are divided into two types: aggregation and awareness. Awareness means that the object that owns the pointer does not bear any responsibility for the object to which it refers. He simply has access to his public fields and methods, but the lifetime of this object is not under his control. Aggregation means that the object owning the link is responsible for the destruction of the object to which it refers. As a rule, an aggregated object lives while the owner object is alive, although there are more complex cases.
In jWidget, the aggregation is implemented through the
own method of the
JW.Class class. By passing object B to object A's own method, you made object A to own object B. When object A is destroyed, object B will be destroyed automatically. For convenience, the own method returns object B. Below is an example of code that uses this feature.
var Soldier = function() { Soldier._super.call(this);
Now we can create a soldier and destroy it by calling the
destroy method.
var soldier = new Soldier(); soldier.destroy();
As a result, we will see the following lines in the browser console:
Destroying soldier
Destroying right hand
Destroying left hand
As you can see, when a soldier is destroyed, the hands are destroyed automatically. Alternatively, we could explicitly destroy the hands in the destroy method of the Soldier class by calling their destroy method. But aggregation allows us to achieve this with less code. In general, in a real application, the destroy method has to be overloaded very rarely. For example, in
my implementation of TodoMVC, this method is never overloaded - everything is achieved by a single object aggregation mechanism.
Aggregated objects are destroyed in the reverse order, which guarantees integrity in the presence of connections between them.
2. Events.
Events are an integral part of any MV * framework. If the view has direct access to the model, then the model knows nothing about the view. Feedback is carried out in no other way than through events. Here, standard user interface events such as
click ,
mousedown, or
keypress are not meant , but events like
“document name has changed” ,
“new document added to folder” ,
“documents in folder are sorted by date” . This is not embedded in the standard programming language tools, so this is the task of the framework.
As I wrote at the beginning of the article, the speed of the script in jWidget is paramount. Therefore, the standard subscription scheme for events, which is offered, for example, in jQuery, does not suit us.
$("#document").bind("click", onClick); $("#document").unbind("click", onClick);
The problem here is that the algorithm for unsubscribing from an event has a linear computational complexity (brute force). I managed to implement a scheme in which the time for unsubscribing from an event is equal to the time for deleting a key from the dictionary, which is much faster. In addition, the jWidget event scheme is implemented according to all OOP principles and is perfectly combined with the object aggregation mechanism.
var Document = function(title) { Document._super.call(this); this.title = title;
The event is represented by the
JW.Event class. Subscribing to an event is returned by the
bind method as an instance of the
JW.EventAttachment class. Destroying a subscription is equivalent to unsubscribing from an event. When we throw an event using the
trigger method, we pass an instance of
JW.EventParams there to pass it to the event handler as an argument.
3. Properties and their helpers
A framework cannot be called a full-fledged MV * framework if it does not provide Data binding capabilities. jWidget provides this feature. Objects of the following classes automatically throw out events about their change, and, therefore, can be used for Data binding:
I will talk about collections (Array, Map, Set) in the next paragraph, but now I would like to explain what a property (
JW.Property ) is.
A property is a "variable" that throws events about a change in its value. Hence the simplest interface of this class:
When you pass the value of x to the set method, the property checks to see if it is equal to this value x. If equal, nothing happens. If not equal, the property assigns itself to the value x and throws the changeEvent event.
Despite the fact that the class interface is simple beyond recognition, it provides ample opportunities for Data binding, which reduces the amount of application code by an order of magnitude. First, we can link two properties by copying the value of one property to another:
var source = new JW.Property("apple"); var target = new JW.Property(); new JW.Copier(source, {target: target}); assertEqual("apple", target.get()); source.set("banana"); assertEqual("banana", target.get());
The
bindTo method does the same thing, which allows us to make the code more understandable. In addition, I draw your attention to the fact that the target property in this case also throws events about its change, so you can link as many properties as you want along the chain:
var source = new JW.Property("apple"); var target1 = new JW.Property(); target1.bindTo(source); var target2 = new JW.Property(); target2.bindTo(target1); source.set("banana"); assertEqual("banana", target2.get());
Copying properties on the fly is just the beginning. Let's try to create a new property based on two existing properties using the formula
text = value + " " + unit
:
var value = new JW.Property(1000); var unit = new JW.Property("MW"); var functor = new JW.Functor([ value, unit ], function(value, unit) { return value + " " + unit; }, this); var target = functor.target; assertEqual("1000 MW", target.get()); value.set(1500); assertEqual("1500 MW", target.get()); unit.set("");
Finally, we tie the text inside some element of the view to the constructed property:
new JW.UI.TextUpdater("#capacity", target);
Now when you change the value and unit, you will automatically update the text inside the #capacity element.
See the
documentation for a complete list of JW.Property class features.
jWidget transfers the usual Data binding through HTML-templates to the JavaScript-code of the application. This provides tremendous opportunities for optimizing the application and expanding its capabilities. Data binding is not limited to the layer between the model and the view. You can easily associate properties within a model and within a view. The algorithm of the application is completely transparent, and you can control what is connected with what, based on the specific use cases of your application. It becomes possible to reuse all the functions of the application. The framework does not perform any precompilation of HTML templates in order to isolate formulas for Data binding from there, thanks to which the speed of the application increases.
The property value can be aggregated using the
ownValue method.
4. Collections and Synchronizers
jWidget introduces 3 native collection classes:
JW.AbstractArray ,
JW.AbstractMap and
JW.AbstractSet . This does not mean that you are not allowed to use native Array and Object - jWidget collections are easily converted to native and vice versa. Each jWidget collection has two implementations — simple and notifying:
-
JW.AbstractArray :
JW.Array and
JW.ObservableArray-
JW.AbstractMap :
JW.Map and
JW.ObservableMap-
JW.AbstractSet :
JW.Set and
JW.ObservableSetSimple collections work a little faster than notifying, but alerting collections emit events about their change, which makes Data binding freely applied to them. Also, classes of simple collections have an identical set of static methods that are designed to perform the same operations with native Array and Object. As an example, I will give the operation of creating an array of view objects by an array of model objects:
In doing so, we simply created a new instance of JW.Array and filled it with view objects. No connection between arrays of documents and their representations has been preserved, so a change in the documents array will not entail a change in the array of representations. To link them together, you need to configure Data binding. In jWidget this is done by creating a synchronizer. In this case, you need to create a
mapper :
function createDocumentViews(documents) { return documents.createMapper({ createItem: function(document) { return new DocumentView(document); }, destroyItem: function(documentView) { documentView.destroy(); }, scope: this }).target; }
As you can see, instead of one kollbek we now transfer two. The second callback is needed so that Mapper can destroy the presentation of the document if it is deleted from the documents array. The mapper forms the
target array and keeps it in full accordance with the source array. When destroying the Mapper, he will destroy all remaining views in the target ... By the way, we forgot to destroy the Mapper. We use aggregation:
var DocumentList = function(documents) { DocumentList._super.call(this); this.documentViews = this.createDocumentViews(documents); }; JW.extend(DocumentList, JW.Class, { createDocumentViews: function(documents) { return this.own(documents.createMapper({ createItem: function(document) { return new DocumentView(document); }, destroyItem: function(documentView) { documentView.destroy(); }, scope: this })).target; } });
Notice how the round brackets stand. We aggregate Mapper, and we return its target.
The
createMapper method works for both JW.Array and JW.ObservableArray. Only in the first case, it will not be able to perform persistent data binding, since JW.Array does not throw out any events. But on the other hand, you can develop an absolutely polymorphic solution with the possibility of replacing JW.Array with JW.ObservableArray at any time, if necessary.
jWidget provides a wide range of synchronizers. See the complete list in the
documentation .
Elements of the collection can be aggregated using the
ownItems method.
5. Components
Finally got to the presentation. jWidget provides the
JW.UI.Component class as the base class for all view components. Each component class has its own template, which is inherited along with this class. A template is plain HTML with 2 new attributes added:
jwclass and
jwid . The template is bound to the component class using the
JW.UI.template method.
var MyComponent = function(message, link) { MyComponent._super.call(this); this.message = message; this.link = link; }; JW.extend(MyComponent, JW.UI.Component, {
The jwclass attribute is set only for the root element of the component, and it is a prefix to the CSS classes of elements. The jwid attribute is a suffix to the CSS class of this element. For example, the above template will expand into the following HTML:
<div class="my-component"> <div class="my-component-hello-message" /> <a href="#" class="my-component-link">Click me!</a> </div>
To render a component in the DOM, you can use the following statement:
var component = new MyComponent("Hello, Wanderer!", "http://google.com"); component.renderTo("body");
In the component code, you can see that using the
getElement method, you can get the jQuery wrapper of an element by its jwid.
The
renderComponent method is a component's life cycle method. By overloading it, you can manipulate the elements of the component and create child components.
Child components are of three types:
- Named child components
- Easily replaceable child components
- Arrays of child components
Named child components completely replace the specified template elements. For example, let an application consist of a title and content. Design them with named child components. This is done by adding them to the
children dictionary:
var Application = function() { Application._super.call(this); }; JW.extend(Application, JW.UI.Component, { renderComponent: function() { this._super(); this.children.set(this.own(new Header()), "header"); this.children.set(this.own(new Content()), "content"); } }); JW.UI.template(Application, { main: '<div jwclass="application">' + '<div jwid="header" />' + '<div jwid="content" />' + '</div>' });
The title and content will be rendered and will replace the “header” and “content” elements. You can add and remove components from the dictionary on the fly, thereby manipulating the contents of the component.
Easily replaceable child components are similar to the named ones, but they are based on JW.Property. They are added by the
addReplaceable method. Such components are conveniently rendered using
JW.Mapper :
var Application = function(selectedDocument) { Application._super.call(this); this.selectedDocument = selectedDocument; }; JW.extend(Application, JW.UI.Component, {
Thus, we implemented Data binding to the selectedDocument property. If you change the value of this property, the representation of the old document will be automatically destroyed, and the representation of the new document will be created and take the place of the old one.
Arrays of child components are based on JW.AbstractArray. They are added by the
addArray method. If the array is JW.ObservableArray, then the method will provide continuous synchronization of the view with this array. Arrays of child components are conveniently rendered via the createMapper method:
var Application = function(documents) { Application._super.call(this); this.documents = documents; }; JW.extend(Application, JW.UI.Component, {
Unlike named and easily replaceable child components, an array does not replace the specified element, but adds child components inside this element. So, you can add an array of child components directly to the root element of the component, by passing the second argument of the addArray method.
For convenience, jWidget allows you to define the renderChildId method, where ChildId is the jwid element recorded in CamelCase with a capital letter. The method accepts a template element as input. Below are the various features of this method:
var Application = function(title, documents, selectedDocument) { Application._super.call(this); this.title = title; this.documents = documents; this.selectedDocument = selectedDocument; }; JW.extend(Application, JW.UI.Component, {
The HTML component template can be rendered into a separate HTML file using the
jWidget SDK . Read more about this in the
Infrastructure section of the
project manual. If the framework finds success, I plan to create a plugin for GruntJS that would replace the jWidget SDK.
I wrote about the jWidget SDK
earlier , but it did not find much support. And now the equivalent GruntJS project appeared, which immediately found enormous support and formed a community. So I turn off the development of the jWidget SDK.
Hope this article was of interest to you. I am quite sure that among JavaScript programmers there will be those who, like me, are fan of real object-oriented programming and appreciate the high speed of code execution. If this is you, try jWidget in your work, and you will not be disappointed. Even with a huge amount of MV * frameworks, I still prefer jWidget. I spend a lot of effort on maintaining documentation, a beginner’s guide and dense unit test coverage. If you want the project to further develop and grow, do not be lazy to put a star on
GitHub and follow me on Twitter
@jwidgetproject . Also, I appreciate constructive criticism and good suggestions. Thank.