
Recently, I have several times managed to participate in discussions about how
Angular is better or worse than
Knockout and other JS frameworks. And very often I have come across the fact that there is some misunderstanding of the essence of the differences in the approaches embedded in these products. Sometimes it even came to the point that, as an advantage of Knockout, valid default “data-” prefixes were cited, which is just completely ridiculous (not to mention the fact that they can be used in Angular).
I want to fix once in this article some thoughts that could later be simply given a link. In my opinion, there are actually three key differences between AngularJS and various other frameworks in different combinations:
- Modular code organization, testability and brutal war with any global data.
- Promoting a declarative approach through creating your own HTML directives.
- The mechanism for checking data changes in data-binding without using callbacks.
And the third point I see here is the most difficult to understand. Let's talk about it.
What is data binding? Roughly speaking, this is a display of data in a template, made so that a change in the data changes their presentation. Having an object:
var myViewModel = { personName: 'Bob', personAge: 123 };
... and the pattern:
<span>personName</span>
... we want the span to
myViewModel
updated to the current state with a minimum of our participation when
myViewModel
changes. Conversely, if it is, for example, an input field.
')
And to solve this problem, there are two fundamentally different approaches.
Change listeners
For the data-binding mechanism in such frameworks as Knockout and Backbone, a change tracking system was developed. You work with data not directly, but through a special layer (for example, observables in KO), which is designed to change the presentation of data in time when it changes. Any variable becomes a framework object that monitors the state.
var myViewModel = { personName: ko.observable('Bob'), personAge: ko.observable(123) }; ... myViewModel.personName('Mary'); myViewModel.personAge(50);
<span data-bind="text: personName"></span>
Clever and obvious solution. It would seem that functional and event-oriented programming in javascript is the best suited for such tasks, and in general work with callbacks is our strong hobby.
However, here there are problems that have to be addressed by such frameworks.
First, what if one part of the data somehow depends on another part? By changing one variable, we automatically report this, but the second variable that has changed will go unnoticed. To resolve such dependencies, KO has a dependency tracking mechanism, which works well, but the very existence of a solution indicates the existence of a problem.
Secondly, the change tracking system is triggered basically at
every change, because basically it’s just callbacks. If we change the data continuously or in large chunks at once, this will cause a lot of unnecessary positives, because the final goal is just to change the display, which one way or another will be reduced to the final form, and intermediate states are not needed here.
Combining solutions for the previous two points, we get the third problem: continuous micro-desynchronization of the entire data state. Each time we change something, we cause a corresponding triggering, which in turn can change other data and also trigger a trigger. In addition to increasing the execution stack in such a way, at each point in time we risk starting to work with data that is being edited (or prepared for it) somewhere else in the stack, and in order to properly track such places in the code you need to very well understand the whole inner kitchen, hidden behind these seemingly simple things; in much the same way as with multi-threaded programming, you need to be very careful about using shared data.
In aggregate, such a data-binding mechanism creates such a complex system that other guys decided to cut the Gordian knot and apply a fundamentally different approach.
Dirty checking
According to this principle, AngularJS works. Dirty checking is a check for altered data, as simple as two rubles. Previously, the myVar variable was 1, now it is 2 - it means the data has changed and it is necessary to redraw them in the template. For simple variables, this is the! = Operator; for complex variables, the corresponding lightweight procedures. This is the simplest approach, which saves us both from the need to work with data through a special “listening” layer, and from all the problems associated with data dependencies.
var myViewModel = { personName: 'Bob', personAge: 123 }; ... myViewModel.personName = 'Mary'; myViewModel.personAge = 50;
<span>{{personName}}</span>
The whole question is when to make this check? Continuously, by timer? Given that the data model can be quite complex, continuously performing checks can severely degrade the UX. In Angular, this issue is solved by automatically calling the
$ digest function after each piece of code that is
supposedly able to change the data . This is a key point - the check is performed if and only if the data could be changed (for example, during a user action), and never is performed in other cases. If you are expecting data to change at some other point in time (for example, when an event is received from a server or when a process is terminated), you must explicitly tell Angular that you should perform the check by calling the
$ apply function.
In this case, the data at each iteration are checked all at once, as a whole, and not just some part. Before and after completion of the execution of $ digest, we have a stable version of the entire state without desynchronization. If, due to data dependencies, one part of them has changed during the verification process, then immediately after the end of the current verification, the next one will be scheduled. And the next check will again be performed entirely, updating the state of the model completely, eliminating possible problems with unrecorded dependencies.
The obvious disadvantage of this approach is performance. Although there is a small exception here: for example, with a batch update of a large amount of data at once, the check is performed only once at the end, rather than with each change of each of the monitored objects, as it happens in the first case. But in general, it is nevertheless a minus, since if only one variable changes, a dirty check of all data is performed.
It is only necessary to understand how strong the loss of productivity.
It is worth noting here that Angular while performing dirty check never works with the DOM. All data is native js objects with which all modern browser engines perform most of the basic operations with lightning speed. Although you can put your own checking procedures into the dirty check process, the Angular documentation strongly recommends that you do not work with the DOM inside them, as this can slow down the entire process.
Given this, we can say that the loss of performance in today's conditions of web applications in practice is not felt. I used to work on developing games for mobile platforms for quite a while, and there (especially on old ones like Palm OS) every extra processor clock usually counted. But even with such a lack of resources, the basic principle of the "date-binding" operation was precisely the simplest dirty check. What is date-binding in the case of a game? This is the display of pictures on the game screen, depending on what happens in the state of the game data. Sometimes, indeed, an approach was used that was close to the approach of the listening callbacks — for example, updating the screen only in those places where the picture changed. But basically, the screen was simply redrawn every time anew, completely replacing the current frame with the new actual state of the graphics. And the only criterion for the validity of such an approach was and remains the FPS indicator - how often we can change the frames in this way and how smooth the corresponding UX will be. As long as the FPS is in the region of the maximum possible for human perception (around 30 frames per second),
there is simply no point in thinking about performance losses, since they do not lead to anything that can be called a deterioration of UX.
The fact is that simple dirty checking, used in AngularJS, allows you to work with data of almost any complexity, and at the same time check and change the display in less than 50ms, and this is even longer than if we only checked part of the data but nonetheless instant for the user. And at the same time, this approach eliminates many different headaches caused by the complex mechanism of change listeners, and simply simplifies the work, because we continue to treat data as ordinary variables and POJO objects, without using the complex syntax of the “listening” layer.