Immediately I warn you that the article is intended only for those who use or intend to use knockoutjs. It is assumed that the reader already knows
what it is and why it is needed .
In one of my projects, I decided to use knockout.js. Everything was good and even wonderful, as long as there was little data and the challenges of computed methods were not resource intensive. But then the data became more and more calculations appeared, which took noticeable time for the eye. Trying to solve this problem, I divided the page into tabs. Turning to a separate tab, I changed the template and thus logically expected that the number of computed values ​​would decrease when any observable value changes. But it was not there. It turned out that the feature of the framework is such that the recalculation of values ​​occurs even when the template is completely removed from the model house.
To make it clearer what you are talking about, I will give a simple example:
There is an input field. When this field is changed, the number of characters entered is calculated, for example, and displayed.
<input data-bind="value:symbols" /> <div data-bind="text:symbolsLength"></div>
var vm={ symbols:ko.observable(''), }; vm.symbolsLength=ko.computed(function(){ return vm.symbols().length; }) ko.applyBindings(vm);
Everything is simple and it works:
jsfiddle
')
Now let's change the example a little. Let's transfer the calculated value to the template and add another template, which will contain static HTML. And make a button, by clicking on which the templates will switch. I expect (more precisely, I would like it to be so) that the value of symbolsLength will not be calculated when this value is not displayed. Therefore, to check this, I will put an alert in the calculation function.
<script id="template1"> <div data-bind="text:symbolsLength"></div> </script> <script id="template2"> static html </script> <input data-bind="value:symbols, valueUpdate: 'afterkeydown'" /> <button data-bind="click:click">Change Template</button> <div data-bind="template:templateName"></div>
var vm={ symbols:ko.observable(''), templateName:ko.observable('template1'), click:function(){ vm.templateName( (vm.templateName()=='template1')? 'template2': 'template1' ); } }; vm.symbolsLength=ko.computed(function(){ alert(1); return vm.symbols().length; }) ko.applyBindings(vm);
Now
let's look at an example and realize what happened. The framework performed the calculation when initializing the symbolsLength property. Thus, he learned from which objects this value depended. In this case, the symbols object was called in the calculation function symbolsLength. The next time you change the symbols, the Knockout will re-calculate the symbolsLength. That is, when we change the value in the input field, the calculation function is called symbolsLength, which is confirmed by an alert.
Also, please note that the alert pops up, even if we switch to another template using the “Change Template” button.
So, two problems arise:
1. The calculation of the computed values ​​during initialization is always called, even if this value is not used anywhere else
2. Even after removing the template in which the computed value was used, from the DOM, the calculation will still occur when the dependent value changes.
The first problem is solved very simply. To do this, you need to pass the parameter deferEvaluation: true to the function ko.computed:
vm.symbolsLength=ko.computed({ read:function(){ alert(1); return vm.symbols().length; }, deferEvaluation:true })
Now, the symbolsLength will be evaluated only after its first use, that is, after adding the template1 template to the DOM. But the second problem remains. The symbolsLength object has already subscribed to the change of the symbols object, and even if symbolsLength is not used anywhere, a calculation will still occur.
The inner voice tells you that the symbolsLength object should somehow be unsubscribed from the symbols.
But how to do that? And the second question: at what point?
I found my answers to these questions and will talk about them further. Perhaps there is a simpler option, which I want to know in the comments.
The answer to the first question: we will not unsubscribe, we will simply create anew the same object. And he will again wait until you need to calculate the value.
The answer to the second question: this can be done during the next calculation of the value, when there are no more subscribers to the result of the calculation. In this case, the subscriber to the symbolsLength calculation is the function of outputting this value in the template1 template.
To determine the number of subscribers and the number of dependencies, the objects in the Knockout have two functions:
getSubscriptionsCount ()
getDependenciesCount ()
In the course of monitoring the values ​​returned by these functions, the following pattern was revealed:
1. At the first calculation, both functions return zero. In this case, we do nothing
2. In the following calculations, these two functions return real values. At the same time, the number of dependencies by definition will be greater than zero, since symbolsLength depends on symbols. But we are interested in the situation when the number of subscribers is zero.
Given the two points described, it is possible to calculate the moment when you need to recreate the object.
So, the theoretical part is solved. We proceed to the practical implementation.
To do this, create a function that will replace ko.computed. Let's call it ko.recompute:
ko.recompute=function(callback){ var rez=function(){ return rez.val(); }; rez.__ko_proto__=ko.observable; var o={ deferEvaluation:true, read:function(){ var s=rez.val.getSubscriptionsCount()==0; var d=rez.val.getDependenciesCount()==0; if(s!=d) { setTimeout(function(){ rez.val.dispose(); rez.val=ko.computed(o); },1); return null; } return callback(); } }; rez.val=ko.computed(o); return rez; };
Now our example will look like this:
var vm={ symbols:ko.observable(''), templateName:ko.observable('template1'), click:function(){ vm.templateName( (vm.templateName()=='template1')? 'template2': 'template1' ); } }; vm.symbolsLength=ko.recompute(function(){ alert(1); return vm.symbols().length; }) ko.applyBindings(vm);
Thus, the calculation of symbolsLength will occur in two cases:
1. When it is necessary to display the values ​​of symbolsLength
2. When the value of symbolsLength is displayed, but there is a change in the value of symbols
And of course the result:
jsfiddle
But a more correct version of the ko.recompute function suggested
xdenser :
ko.recompute=function(callback){ var c; function create(){ c = ko.computed({ read: callback, deferEvaluation:true, disposeWhen:function(){ return (c.getSubscriptionsCount()==0)&& setTimeout(create,0); } }); } create(); function read(){ return c(); } read.__ko_proto__=ko.observable; return read; };
This uses the special parameter disposeWhen. Thus, the test will take place not in the calculation function, but in front of it.