Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  • Is that right?
  • Then why should I make use of ko.computed() if a simple function gets me the current selected Item every time I call it, while ko.computed gets evaluated at anytime where everything is done already and I dont need it anymore?
like image 591
Jürgen Zornig Avatar asked Oct 21 '22 20:10

Jürgen Zornig


1 Answers

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/

like image 60
Tomalak Avatar answered Oct 27 '22 11:10

Tomalak