Search code examples
javascriptknockout.jsknockout-2.0knockout-3.0

When is Knockout's ObservableArray subscribe called


Any knockout obervable's subscribe() should be called only when there is a change in the value. But in the below code snippet even though same value is assigned twice to the observable, subscribe is getting called. I can understand that once subscribe will be called (i.e. when first time x is assigned a value of y) but what I do not understand is why is it getting called the second time, when there is no change in the assigned value? Can someone please help me understand the working of subscription?

// This is a simple *viewmodel* - JavaScript that defines the data and behavior of your UI
function AppViewModel() {
    var x = ko.observableArray([]);
    var y = [1]; 
    var oldVal = '';   
    x.subscribe(function(newVal){
   
      alert("newVal: "+newVal+" oldVal: "+oldVal);
       console.log("newVal: "+newVal+" oldVal: "+oldVal);
      var type = newVal instanceof Array;
      alert(type);
      oldVal = newVal;
    });    
    x(y);        
    x(y);
   
}

// Activates knockout.js
ko.applyBindings(new AppViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>


Solution

  • Any knockout obervable's subscribe() should be called only when there is a change in the value.

    This isn't entirely true, according to the knockout maintainers.

    Knockout uses an equalityComparer to determine whether a new value differs from the old one:

    console.log(
      ko.observable.fn.equalityComparer
    )
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-debug.js"></script>

    As you can see, it only actually does a === if the values are primitive. Any other values, like objects, arrays and functions, just always trigger a change.

    I usually "fix" this by overwriting the default:

    const someObj = {};
    const someObs = ko.observable(someObj);
    
    someObs.subscribe(console.log);
    
    // Logs {}
    console.log("with regular eq comparer");
    someObs(someObj);
    
    // Change equalityComparer for *every* observable:
    ko.observable.fn.equalityComparer = (a, b) => a === b;
    
    // No longer logs:
    console.log("with custom eq comparer");
    someObs(someObj);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

    If you don't want to overwrite all equalityComparers, you can also change it on just one observable instance.

    The reasoning for the default behavior, is that many people expect this to trigger updates: (which it doesn't when you use my custom comparer)

    const someObj = { a: 1 };
    const someObs = ko.observable(someObj);
    
    someObj.a = 2;
    someObs(someObj)