Reevaluating a Knockout computed which depends jus

2019-07-04 15:59发布

My Appmodel consists of an observable array of comments

self.comments=ko.observableArray([]);  // Default Value is an empty array
/*
   Here comes some code to initially fill the observable array 
   with items from an JSON Response
*/

Furthermore I have two computeds which should represent the very first comment and the last comment

self.firstComment = ko.computed(function () {
    var sorted = self.comments.sort(function (left, right) {
        return left.Id() - right.Id();
    });
    return sorted[0];
});

self.lastComment = ko.computed(function () {
    var sorted = self.comments.sort(function (left, right) {
        return left.Id() - right.Id();
    });
    return sorted[sorted.length - 1];
});

This works perfectly on initializing the application (loading the JSON from Server, build up App model...), but when I add a comment to the array, the computeds do not recognize that the number of array items has changed (as I understood it, an observable array is just an observable where the array properties themselves are observed). So when I do:

self.comments.push(aNewCommentObject);

self.lastComment is still bound to the array item, that it was when the app loaded initially.

I have found this blog post how to force computation by introducing a dummy observable, but I don't like the approach. For what purpose is an observableArray used then and how?

Additional Challenge: I would like to keep the observableArray Items sorted under every circumstance (because its a comment feed which should be just sorted chronologically). I tried to do this whith an computed commentsSorted but also have problems that this does not update when the observableArray has new items, so same problem here. Thats the reason, why I am sorting everytime in firstComment and lastComment.

3条回答
戒情不戒烟
2楼-- · 2019-07-04 16:23

Try unwrapping the comments to trigger Knockout's dependency tracking:

self.firstComment = ko.computed(function () {
    var sorted = self.comments().sort(function (left, right) {
        // -------------------^^ !
        return left.Id() - right.Id();
    });
    return sorted[0];
});

or (same thing):

self.firstComment = ko.computed(function () {
    var sorted = ko.unwrap(self.comments).sort(function (left, right) {
        return left.Id() - right.Id();
    });
    return sorted[0];
});

Of course you can abstract this into a separate computed.

self.commentsSorted = ko.computed(function () {
    return self.comments().sort(function (left, right) {
        return left.Id() - right.Id();
    });
});

self.firstComment = ko.computed(function () {
    return ko.unwrap(self.commentsSorted)[0];
});

Since computeds cache their return values (just like every other observable does) you don't need to worry about calling self.commentsSorted multiple times. It will re-calculate only when the underlying observable array chances.

查看更多
Root(大扎)
3楼-- · 2019-07-04 16:28

How can you "keep the observableArray items sorted under every circumstance"? My advice is to not try to sort the original array (I'll describe the pitfalls in more detail below) but to use a computed observable that returns a new, sorted array:

self.commentsSorted = ko.computed(function () {
    return self.comments.slice(0).sort(function (left, right) {
        return left.Id() - right.Id();
    });
});

An important thing to understand about the JavaScript Array sort() method is that it changes the original array. Knockout's observableArray wraps many of the array functions so they can be used directly on the observableArray object. That's why you can use myObservableArray.sort() instead of having to do myObservableArray().sort(). But the two aren't equivalent because Knockout wraps array mutator methods differently, approximately like this:

sort: function (sortFunction) {
    this.valueWillMutate();
    var result = this.peek().sort(sortFunction);
    this.valueHasMutated();
    return result;
}

So what's wrong with automatically changing the original observableArray?

  1. It's hard to grasp what's going on. It's easy to either not get a dependency on the array and thus never update past the initialization, or to not notify of the change and thus break other elements from getting the correct view of the array.

  2. Even if you do set it up correctly, you've now created a circular dependency because you have something that's updated whenever the array changes, which then updates the array again. Although Knockout currently disables this for synchronous updates of a computed observable (but that might change in the future), it will result in unbounded recursion if using a manual subscription or a computed with the throttle option.

查看更多
不美不萌又怎样
4楼-- · 2019-07-04 16:33

Alternative solution, using subscribe:

self.comments = ko.observableArray([]);

self.comments.subscribe(function(comments) {
    var sorted = comments.sort(function (left, right) {
        return left.Id() - right.Id();
    });

    self.firstComment(sorted[0]);
    self.lastComment(sorted[sorted.length - 1]);

});
查看更多
登录 后发表回答