3
votes

I have an observable A that is bound to an UI element. I also have a computed B that depends on A. I have a computed ะก that depends on both A and B. I have a subscription to C

When value in UI element is changed then computed is evaluated twice and subscription is called twice.

I think the reason is that A has two subscriptions: A: [B, C]
Knockout notifies B about changes in A.
After B was evaluated it notifies C about changes in B
Then it goes back to the start and calls second subscription of A which is C.
Here we have two calls to C.

Is there a way to prevent this?

var viewModel = {
    firstName: ko.observable("Andrew"),
    lastName: ko.observable("King"),
};

viewModel.fullName = ko.computed(function() {
    return viewModel.firstName() + " " + viewModel.lastName();
});

viewModel.user = ko.computed(function() {
    return {
    fullName: viewModel.fullName(),
    lastName: viewModel.lastName()
  };
});

viewModel.user.subscribe(function() {
    // This is called once if I change first name
    // It is called twice if I change last name
});

http://jsfiddle.net/jngxwf5v/

1

1 Answers

4
votes

The observable recomputes when one of its dependencies changes. Since you're creating a new object on every run, knockout won't be able to tell if something actually changes.

Fix it using a deferred computed

To prevent it from running multiple times when both of its dependencies change, you can make it deferred:

var viewModel = {
    firstName: ko.observable("Andrew"),
    lastName: ko.observable("King"),
};

viewModel.fullName = ko.computed(function() {
    return viewModel.firstName() + " " + viewModel.lastName();
});

viewModel.user = ko.computed(function() {
    return {
    fullName: viewModel.fullName(),
    lastName: viewModel.lastName()
  };
}).extend({ deferred: true });

viewModel.user.subscribe(function(user) {
  console.log(user);
});

viewModel.firstName("John");
viewModel.lastName("Doe");
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

Fix it using a custom equality comparer

Another way of fixing the issue, is by adding a custom equality comparer. This lets knockout check if, when a dependency changes, the new outcome actually differs from the previous one. Only if the two differ, subscribers are updated.

var viewModel = {
    firstName: ko.observable("Andrew"),
    lastName: ko.observable("King"),
};

viewModel.fullName = ko.computed(function() {
    return viewModel.firstName() + " " + viewModel.lastName();
});

viewModel.user = ko.computed(function() {
  return {
    fullName: viewModel.fullName(),
    lastName: viewModel.lastName()
  };
});

viewModel.user.equalityComparer = (x, y) => x === y || x.fullName === y.fullName && x.lastName === y.lastName;

viewModel.user.subscribe(console.log);

viewModel.lastName("Doe");
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

Differences between the two approaches

In the deferred example, knockout sort of pushes the re-execution of the computed to a setTimeout. It will only run once, but you won't know "when".

In the second example, the computed function is called twice (like before). The only difference is that subscribers aren't notified, because the two outcomes are considered equal.