Jürgen Zornig
Jürgen Zornig

Reputation: 1244

Knockout computed vs. subscription, timing issues

Just found out, that in KnockoutJS subscription functions are evaluated before dependent computables and need someone who can commit that, because I can't find anything about Knockouts timing in the docs or discussion forums.

That means: If I have a model like this...

var itemModel = function (i) {
    var self = this;

    self.Id = ko.observable(i.Id);
    self.Title = ko.observable(i.Title);
    self.State = ko.observable(i.State);

};

var appModel = function () {
   var self = this;

   self.Items = ko.observableArray() // <-- some code initializes an Array of itemModels here
   self.indexOfSelectedItem = ko.observable();

   self.selectedItem = ko.computed(function () {
       if (self.indexOfSelectedItem() === undefined) {
            return null;
       }
       return self.Items()[self.indexOfSelectedItem()];
   });
};

where I want to keep track of the selected array item with an observable index field, and I subscribe to this index field like this...

appModel.indexOfSelectedItem.subscribe(function () {
    // Do something with appModel.selectedItem()
    alert(ko.toJSON(appModel.selectedItem()));
}

...the subscription function is evaluated before the computed is reevaluated with the new index value, so I will get the selectedItem() that corresponds to the last selected Index and not the actual selected Index.

Two questions:

Upvotes: 5

Views: 2469

Answers (1)

Tomalak
Tomalak

Reputation: 338406

By default all computeds in Knockout are evaluated in an eager fashion, not lazily (i.e., not when you first access them).

As soon as one dependency changes, all all subscriptions are notified and all connected computeds are re-evaluated. You can change that behavior to "lazy" by specifying the deferEvaluation option in a computed observable, but you cannot do this for a subscription.

Hoewever, I think there is no need to depend on the index of the selected item. In fact, that's even bad design because you are not really intested in the numerical value of the index, but rather in the item it represents.

You could reverse the dependencies by creating a writeable computed observable that gives you the index of the currently selected item (for diplay purposes) and allows to change it as well (for convenience).

function AppModel() {
    var self = this;

    self.Items = ko.observableArray();
    self.selectedItem = ko.observable();

    self.indexOfSelectedItem = ko.computed({
        read: function () {
            var i,
                allItems = self.Items(),
                selectedItem = self.selectedItem();

            for (i = 0; i < allItems.length; i++) {
                if (allItems[i] === selectedItem) {
                    return i;
                }
            }
            return -1;
        },
        write: function (i) {
            var allItems = self.Items();

            self.selectedItem(allItems[i]);
        }
    });
}

Knockout favors storing/handling the actual values instead of just indexes to values, so it would probably not be difficult to make the necessary changes to your view. Just make everything that previously wrote to indexOfSelectedItem now write to selectedItem directly. Dependencies on selectedItem will continue to work normally.

In a well-designed Knockout application you will rarely ever have the need to handle the index of an array item. I'd recommend removing the write part of the computed once everything works.

See: http://jsfiddle.net/4hLLn/

Upvotes: 3

Related Questions