Search code examples
knockout.jsknockout-subscribe

Knockout subscribe is evaluating unexpectedly


I need a timestamp for when a value is updated. For reasons I won't go into here, the value is a writable computed that points to a valueInstance observable, so they basically show the same data.

If I subscribe to the observable it works as expected, only fires when the observable changes. If I subscribe to the computed it fires immediately causing a false timestamp, even though the observable is still undefined. What's up with this?

http://jsfiddle.net/bNXhm/

Update: looks like this only happens when the computed has deferEvaluation: true

http://jsfiddle.net/bNXhm/1/

function VM(){
    var self = this;

    self.valueInstance = ko.observable();
    self.value = ko.computed({
        read: function () {
            return self.valueInstance();
        },
        write: function (value) {
            self.valueInstance(value);
        },
        deferEvaluation: true
    });

    self.timeStamp1 = ko.observable();
    self.value.subscribe(function (newValue) {
        self.timeStamp1(new Date());
    });

    self.timeStamp2 = ko.observable();
    self.valueInstance.subscribe(function (newValue) {
        self.timeStamp2(new Date());
    }); }

Solution

  • When you create an observable it will be evaluated unless you set deferEvaluation to true. When an observable (or computed) is evaluated it always notify them subscribers. There no comparison with the previous value at this point (in order to notify only if the value changes).

    The "useless notification check" is made in the setter of the observable.

    So in your code, when you use deferEvaluation at false, the read function is immediately evaluated and the subscribers will are notifed. But there is no subscribers at this time. That's why the timespan is not set.

    And when you use deferEvaluation : true, the evaluation process is delay in the applyBindings function. At this time you already subscribed and that's why the timespan will is set.

    To solve your problem, I would create a variable to store when you should start to logged changes.

    function VM(){
    
        var self = this;
        self.logged  = false;
    
    
        self.valueInstance = ko.observable();
        self.value = ko.computed({
            read: function () {
                return self.valueInstance();
            },
            write: function (value) {
                self.valueInstance(value);
            },
            deferEvaluation: true
        });
    
        self.timeStamp1 = ko.observable();
        self.value.subscribe(function (newValue) {
            if(self.logged)
                self.timeStamp1(new Date());
        });
    
        self.timeStamp2 = ko.observable();
        self.valueInstance.subscribe(function (newValue) {
            if(self.logged)
                 self.timeStamp2(new Date());
        });
    }
    var vm  = new VM();
    ko.applyBindings(vm);
    vm.logged = true;
    

    I hope it helps

    See fiddle