简体   繁体   中英

Knockout computed property fires on loading

I'm starting with knockout and my computed observable seems to fire always when the viewmodel is instantiated and i don't know why.

I've reduced the problem to the absurd just for testing: the computed property just prints a message in the console and it is not binded to any element at the DOM. Here it is:

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

(...)

        self.FullName = ko.computed(function () {
            console.log("INSIDE");
        });

(...)

    };
    ko.applyBindings(new HomeViewModel());
})();

How can it be avoided?

Update:

Here is the full code of the ViewModel just for your better understanding:

function HomeViewModel() {
    var self = this;

    self.teachers = ko.observableArray([]);
    self.students = ko.observableArray([]);

    self.FilterByName = ko.observable('');
    self.FilterByLastName = ko.observable('');

    self.FilteredTeachers = ko.observableArray([]);
    self.FilteredStudents = ko.observableArray([]);

    self.FilteredUsersComputed = ko.computed(function () {
        var filteredTeachers = self.teachers().filter(function (user) {                
            return (user.name.toUpperCase().includes(self.FilterByName().toUpperCase()) &&
                user.lastName.toUpperCase().includes(self.FilterByLastName().toUpperCase())
            );
        });
        self.FilteredTeachers(filteredTeachers); 
        var filteredStudents = self.students().filter(function (user) {
            return (user.name.toUpperCase().includes(self.FilterByName().toUpperCase()) &&
                user.lastName.toUpperCase().includes(self.FilterByLastName().toUpperCase())
            );
        });
        self.FilteredStudents(filteredStudents);
        $("#LLAdminBodyMain").fadeIn();
    }).extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 800 } });

    self.FilteredUsersComputed.subscribe(function () {
        setTimeout(function () { $("#LLAdminBodyMain").fadeOut(); }, 200);
    }, null, "beforeChange");

    $.getJSON("/api/User/Teacher", function (data) {            
        self.teachers(data);   
    });
    $.getJSON("/api/User/Student", function (data) {
        self.students(data);
    });
}

ko.applyBindings(new HomeViewModel());

})();

I need it to not be executed on load because on load the self.students and self.teachers arrays are not jet populated.

NOTE: Just want to highlight that in both codes (the absurd and full), the computed property is executed on loading (or when the ViewModel is first instantiated).

There are two main mistakes in your approach.

  • You have a separate observable for filtered users. That's not necessary. The ko.computed will fill that role, there is no need to store the computed results anywhere. (Computeds are cached, they store their own values internally. Calling a computed repeatedly does not re-calculate its value.)
  • You are interacting with the DOM from your view model. This should generally be avoided as it couples the viewmodel to the view. The viewmodel should be able operate without any knowledge of how it is rendered.

Minor points / improvement suggestions:

  • Don't rate-limit your filter result. Rate-limit the observable that contains the filter string.
  • Don't call your computed properties ...Computed - that's of no concern to your view, there is no reason to point it out. For all practical purposes inside your view, computeds and observables are exactly the same thing.
  • If teachers and students are the same thing, ie user objects to be displayed in the same list, why have them in two separate lists? Would it not make more sense to have a single list in your viewmodel, so you don't need to filter twice?
  • Observables are functions. This means
    $.getJSON("...", function (data) { someObservable(data) });
    can be shortened to
    $.getJSON("...", someObservable); .

Here is a better viewmodel:

function HomeViewModel() {
    var self = this;

    self.teachers = ko.observableArray([]);
    self.students = ko.observableArray([]);

    self.filterByName = ko.observable().extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 800 } });
    self.filterByLastName = ko.observable().extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 800 } });

    function filterUsers(userList) {
        var name = self.filterByName().toUpperCase(),
            lastName = self.filterByLastName().toUpperCase(),
            allUsers = userList();

        if (!name && !lastName) return allUsers;

        return allUsers.filter(function (user) {
            return (!name || user.name.toUpperCase().includes(name)) &&
                (!lastName || user.lastName.toUpperCase().includes(lastName));
        });
    }

    self.filteredTeachers = ko.computed(function () {
        return filterUsers(self.teachers);
    });
    self.filteredStudents = ko.computed(function () {
        return filterUsers(self.students);
    });
    self.filteredUsers = ko.computed(function () {
        return self.filteredTeachers().concat(self.filteredStudents());
        // maybe sort the result?
    });

    $.getJSON("/api/User/Teacher", self.teachers);
    $.getJSON("/api/User/Student", self.students);
}

With this it does not matter anymore that the computeds are calculated immediately. You can bind your view to filteredTeachers , filteredStudents or filteredUsers and the view will always reflect the state of affairs.


When it comes to making user interface elements react to viewmodel state changes, whether the reaction is "change HTML" or "fade in/fade out" makes no difference. It's not the viewmodel's job. It is always the task of bindings.

If there is no "stock" binding that does what you want, make a new one . This one is straight from the examples in the documentation :

// Here's a custom Knockout binding that makes elements shown/hidden via jQuery's fadeIn()/fadeOut() methods
// Could be stored in a separate utility library
ko.bindingHandlers.fadeVisible = {
    init: function(element, valueAccessor) {
        // Initially set the element to be instantly visible/hidden depending on the value
        var value = valueAccessor();
        $(element).toggle(ko.unwrap(value)); // Use "unwrapObservable" so we can handle values that may or may not be observable
    },
    update: function(element, valueAccessor) {
        // Whenever the value subsequently changes, slowly fade the element in or out
        var value = valueAccessor();
        ko.unwrap(value) ? $(element).fadeIn() : $(element).fadeOut();
    }
};

It fades in/out the bound element depending the bound value. It's practical that the empty array [] evaluates to false, so you can do this in the view:

<div data-bind="fadeVisible: filteredUsers">
  <!-- show filteredUsers... --->
</div>

A custom binding that fades an element before and after the bound value changes would look like follows.

  • We subscribe to value during the binding's init phase.
  • There is no update phase in the binding, everything it needs to do is accomplished by the subscriptions.
  • When the DOM element goes away (for example, because a higher-up if or foreach binding triggers) then our binding cleans up the subscriptions, too.

Let's call it fadeDuringChange :

ko.bindingHandlers.fadeDuringChange = {
    init: function(element, valueAccessor) {
        var value = valueAccessor();

        var beforeChangeSubscription = value.subscribe(function () {
            $(element).delay(200).fadeOut();
        }, null, "beforeChange");

        var afterChangeSubscription = value.subscribe(function () {
            $(element).fadeIn();
        });

        // dispose of subscriptions when the DOM node goes away
        // see http://knockoutjs.com/documentation/custom-bindings-disposal.html
        ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
            // see http://knockoutjs.com/documentation/observables.html#explicitly-subscribing-to-observables
            beforeChangeSubscription.dispose();
            afterChangeSubscription.dispose();
        });
    }
};

Usage is the same as above:

<div data-bind="fadeDuringChange: filteredUsers">
  <!-- show filteredUsers... --->
</div>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM