I'm faced a problem that my computed observable stops triggering after some sequence of dependency changes. Finally I found out the point: if dependency was inside of false branch statement during latest evaluation, computed will not be triggered next time even if condition became true before evaluation finished.
Here is a sample: https://jsfiddle.net/sgs218w0/1/
var viewModel = new function(){
var self = this;
self.trigger = ko.observable(true);
self.fire = function(){
self.trigger(! self.trigger());
};
self.content = function(){
var test = 3;
return ko.computed(function(){
alert("triggered!");
if(test !== 0){
console.log(self.trigger());
alert(test);
}
test--;
});
}();
};
ko.applyBindings(viewModel);
Is it bug or feature? Do you know any workaround for this issue? I seems to be optimization, but it looks aggressive and incorrect for me. (Edit: I changed my mind. It is reasonable, but can lead to some issues sometimes. I think knockout should have options to fix this issues)
P.S. I could publish more detailed example of real code to make question more specific, if you need it. But the point of real code it the same.
UPDATE
Well, I had to be less lazy to provide more detailed example of what I want achieve. I like the idea of computed which automatically make ajax calls. Described here. One disadventure I see is that call will be made even if corresponding part of UI is invisible. I tried to fix it this way: https://jsfiddle.net/bpr88bp3/1/. The problem is that once tab is deativated it can't be activated anymore, because computed stops triggering...
After reading the update to your question and looking through the updated example code, I've come up with a real solution. This uses a pureComputed
to do the update, taking advantage of the fact that a pure computed can be activated and deactivated by subscribing to it and disposing the subscription. Here is the important code:
updateComputed = ko.pureComputed(function () {
updateTrigger();
result(evaluator.call(owner));
});
ko.computed(function () {
var isActive = result.active();
if (isActive && !updateSubscription) {
updateSubscription = updateComputed.subscribe(function () {});
} else if (updateSubscription && !isActive) {
updateSubscription.dispose();
updateSubscription = undefined;
}
});
https://jsfiddle.net/mbest/bpr88bp3/2/
According to the Knockout JS documentation:
So, Knockout doesn’t just detect dependencies the first time the
evaluator runs - it redetects them every time.
When if(test !== 0){
is false, Knockout does not subscribe to self.trigger()
due to self.trigger()
not being called during computed recalculation.
With no subscription to the self.trigger()
, there is no recalculation of the computed on further self.trigger()
changes.
IMHO the workaround is to get self.trigger()
in any case (updated fiddle):
self.content = function(){
var test = 3;
return ko.computed(function(){
var triggerValue = self.trigger();
alert("triggered!");
if(test !== 0){
console.log(triggerValue);
alert(test);
}
test--;
});
}();
The idea, in general, is that all dependencies of a computed observable should also be observables. In your case, test
isn't an observable; if you make it observable, your code should work as expected:
self.content = function(){
var test = ko.obseravble(3);
return ko.computed(function(){
alert("triggered!");
if(test() !== 0){
console.log(self.trigger());
alert(test);
}
test(test()-1);
});
}();
But this introduces another problem because you're now changing one of the dependencies within your computed observable. By changing test
, logically, the computed should be evaluated recursively. If the computed is synchronous, Knockout prevents this, but this is not really a feature, and it could cause problems later.
Here's what I think is a better solution that properly isolates the meanings of the components:
self.content = function(){
var respondToTriggerChange = ko.obseravble(3);
self.trigger.subscribe(function () {
respondToTriggerChange(respondToTriggerChange()-1);
});
return ko.computed(function(){
alert("triggered!");
if(respondToTriggerChange()){
console.log(self.trigger());
alert(test);
}
});
}();