Now that you’ve seen what a ViewModel can do, let’s dig in a bit on what is happening under the covers. Having a basic understanding of the mechanics will help you diagnose issues because you will know what the ViewModel does with your declarations.
The ViewModel has two distinct jobs: managing changes to the data object and scheduling the bindings when data changes.
ViewModel Data and Inheritance
The ViewModel class manages a “data” object and it leverages the JavaScript prototype chain to provide value inheritance. Said in picture form:
This means that all components can read properties set by the top-level container (stored in its data object, “Data 1”). Say we have this in Container 1:
viewModel: {
data: {
foo: 42
}
}
This allows all components to bind to {foo}
. This is typically used to track important records (like “currentUser” perhaps) that are needed at many levels of the application. In fact, because of the use of the JavaScript prototype chain to represent the data, publishing an object in the ViewModel is often a good idea if changes to properties are to be shared as well. To see why, consider a two-way binding to {foo}
but in a child of Container 2:
{
xtype: 'textfield',
bind: '{foo}'
}
The text field will receive “42” via Data 2’s prototype chain ultimately from Data 1. Changes made by this component, however, are stored on Data 2. This is because these components are bound to their ViewModel and its data object which means the two-way nature of the bind will effectively call set
on ViewModel 2 which, acting as a normal JavaScript object, calls sets foo
on Data 2. This “forking” can be a useful way to initialize values that are then separate by view.
To share “live” properties via inheritance, an object should be stored in the root ViewModel instead:
viewModel: {
data: {
stuff: {
foo: 42
}
}
}
Now the two-way binding will update the “foo” property on the shared “stuff” object:
{
xtype: 'textfield',
bind: '{stuff.foo}'
}
Scheduling and Dependencies
The key to making data binding fast is to avoid redundant or unnecessary calculation.
To manage this, the ViewModel tracks the dependencies between data. Every binding and formula introduces a dependency. Under the hood, a ViewModel breaks these down one indirection at a time and creates a linear schedule. This schedule is processed on a delay when data is changed.
So if you set a value in the ViewModel or change a field of a record, you don’t need to worry that a flood of recalculations will immediately take place. Likewise, if a formula depends on 7 values and you need to change all of them, the formula will only be recalculated once. To take it further, if you have 7 formulas, each using the other (so a chain 7 deep), and each depends on 7 other values, changes to all of the 49 values will cause each formula to be recalculated just once.
To achieve this, these dependencies have to be known to the ViewModel and they have to be acyclical. A cycle in the dependency graph is reported as an error. This is easy to see in this contrived example:
Ext.define('App.view.broken.BrokenModel', {
extend: 'Ext.app.ViewModel',
formulas: {
bar: function (get) {
return get('foo') / 2;
},
foo: function (get) {
return get('bar') * 2;
}
}
});
In real applications these bugs will likely not be so obvious, but clearly “foo” and “bar” depend on each other so there can be no sequence of evaluation for these two methods that yields a proper answer.
Formula Dependencies
When a formula uses an explicit bind then its dependencies are obvious. When a formula just provides a function or a get
method, then the ViewModel parses the text of the function looking for property references. While this is extremely convenient and ensures that you don’t forget to list a dependency in an explicit bind, there are limitations to this approach. For details on formula parsing, see Ext.app.bind.Formula.