3

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...

shameleo
  • 344
  • 2
  • 13
  • This is a great question. I *think* it's a feature, though an unexpected one at that. Obviously you can [work around it by saving `self.trigger()` outside the `if`](https://jsfiddle.net/hzrntk3m/), but it's not nice to need that extra bit of code... – Jeroen Mar 16 '16 at 13:33
  • I guess it would not hurt to cross-post to [the forums](https://groups.google.com/forum/#!forum/knockoutjs) as the maintainers might be able to chip in on whether this is intended behavior or not... – Jeroen Mar 16 '16 at 13:34
  • Seems like it looses it's dependency once it is "out of the loop". Interesting. Perhaps it's away getting rid of garbage. If you do post to the form, please post link in question. – Michael Mar 16 '16 at 15:30

3 Answers3

3

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--;
    });
  }();
Roy J
  • 42,522
  • 10
  • 78
  • 102
TSV
  • 7,538
  • 1
  • 29
  • 37
  • Useful link, it answers on "why", but not on "how to prevent". So I can't accept this answer. The question is updated. – shameleo Mar 17 '16 at 08:57
2

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);
    }
  });
}();
Michael Best
  • 16,623
  • 1
  • 37
  • 70
  • My first sample is a bit incorrect, see updated question. The point is that what I call "trigger" is not actually observable, but external function which is dependent from observables. And it should not be executed, if condition is not met – shameleo Mar 17 '16 at 08:47
  • By the way, making "test" variable observable changes nothing: https://jsfiddle.net/sgs218w0/3/ – shameleo Mar 18 '16 at 06:04
  • Once `test` becomes zero, the computed is only dependent on `test` meaning that you would have to modify `test` to trigger it. I guess the real question is why do you want to evaluate the computed observable again? – Michael Best Mar 18 '16 at 21:34
  • Okay, I just read your added information in the question. Please take a look at http://www.knockmeout.net/2011/04/pausing-notifications-in-knockoutjs.html and see if it helps. – Michael Best Mar 18 '16 at 21:37
  • Pauseable computed emulation has leak which make it inappropriate in my case. When I call "resume" it will always trigger. That means, my tab will make ajax call every time when I switch to it. I need it would trigger only if dependencies of my paused computed actually changed their value. I doubt It can be implemented without build-in support... – shameleo Mar 20 '16 at 09:13
2

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/

Michael Best
  • 16,623
  • 1
  • 37
  • 70