knockout.jscomputed-observable

knockout writeable computed causes unnecessary 'read' calls


Computed read/write problem: 'write' causes 'read' execution.

I have read/write computed (or could also be pureComputed). It is writeable, because it could be computed from startDate and endDate. However, it could be also set directly, thus it is 'readable' and based on value startDate and endDate are set. Here is simplified version of my model:

 self.startDate = ko.observable(...);
 self.endDate = ko.observable(...);
 self.selectedTimePeriod = ko.pureComputed({
            read: function () {                
                console.log('time period read');
                var startDate = self.startDate(),
                    endDate = self.endDate(),
                    timePeriod = Enums.TimePeriodTypeEnum.None;
                timePeriod = Constants.MTD;                
                return timePeriod;
            },
            write: function (timePeriodValue) {
                console.log('time period write');
                switch (timePeriodValue) {
                    case Constants.MTD:
                        self.startDate(...);
                        self.endDate(...);
                        break;
                }
            }
    });

    self.timePeriodChange = function () {
        self.selectedTimePeriod(Constants.MTD);
    }

User click on UI triggers self.timePeriodChange function. As a result in console I see the following:

time period write
time period read
time period read

Thus, 'write' part is executed, however, when I change startDate and endDate - each time also 'read' part is executed. I see that this is because write updates read - but what if I do not wish this? How to deal with this situation?

The suggestion was to use peek, but this causes other problems (observable is not updated). Here is my fiddler: https://jsfiddle.net/o5kacas3/ (changing dropdown, would not change computed on UI and actually its value, even when write part is executed).


Solution

  • What you're describing isn't actually a problem; it's how write works.

    In the read method, knockout creates a subscription to any observable you evaluate. It needs to create these subscriptions so it can reevaluate its own value whenever another value it depends on changes.

    If you use obs.peek() instead of just obs(), you can use a value without creating a subscription. This means your computed will not automatically update in the future.

    In the write method, you're setting the observables that determine the read value. If you set 2 observables the computed relies on, you'll trigger 2 reads.

    To illustrate: by setting the computed in the example below, and logging in between changes, you'll see the value actually change to "CB" briefly:

    var reads = 0;
    var a = ko.observable("A");
    var b = ko.observable("B");
    
    // Read 0 is triggered by the computed itself: 
    // it wants to know its initial value
    var c = ko.computed({
      read: function() {
        // This line creates subscriptions to both `a` and `b`
        var value = a() + b();
        console.log("READ " + reads++ + ": " + value);
        return value;
      },
      write: function(str) {
        console.log("WRITE");
        
        a(str[0]); // Triggers read 1, since a changed
        
        // Momentarily, the value is 'CB'
        
        b(str[1]); // Triggers read 2, since b changed
      }
    });
    
    c("CD");
    <script src="http://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>

    Edit: from the comments I now understand that it's essential to the op to not trigger multiple reads. The solution I proposed in the comments:

    Rewrite the code to circumvent the writable observable:

    var a = ko.observable("A");
    var b = ko.observable("B");
    
    var getInitialValue = function() {
      return a() + b();
    };
    
    var createNewValues = function(str) {
      a(str[0]);
      b(str[1]);
    };
    
    
    var c = ko.observable(getInitialValue());
    c.subscribe(createNewValues);
    
    
    console.log(c()); // AB
    c("CD");
    console.log(c()); // CD
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>