Force a computed property function to run

2019-03-08 07:26发布

问题:

Given a computed property

vm.checkedValueCount = ko.computed(function(){
  var observables = getCurrentValues();  //an array of ko.observable[]
  return _.filter(observables, function(v) { return v() }).length;
});

suppose getCurrentValues() can return different sets of observables which are modified elsewhere in the code (and comes from a more complex structure than an observableArray).

I need checkedValueCount to update whenever

  • one of its dependencies change
  • getCurrentValues() returns a different set of observables.

The problem is that ko.computed seems to memoize the last returned value and only update when a dependency updates. This handles the first case but not the latter.

What I'm looking for is a way to force checkedValueCount to re-run. Something which I can use like:

changeCurrentValues();
vm.checkeValueCount.recalculate();

Put most simply, given that I have

a = ko.computed(function() { return Math.random() })

how can I force invoking a() twice to return different values.

回答1:

I realized my first answer missed your point, and won't solve your issue.

The problem is that a computed will only reevaluate if there is some observable that forces it to re-evaluate. There is no native way to force a computed to re-evaluate.

However, you can get around this with some hackery by creating a dummy observable value and then telling its subscribers that it has changed.

(function() {

    var vm = function() {
        var $this = this;

        $this.dummy = ko.observable();

        $this.curDate = ko.computed(function() {
            $this.dummy();
            return new Date();
        });

        $this.recalcCurDate = function() {
            $this.dummy.notifySubscribers();
        };        
    };

    ko.applyBindings(new vm());

}());​

Here is a Fiddle showing this approach



回答2:

There is a method to force recalculation of all observables depending on yours:

getCurrentValues.valueHasMutated()


回答3:

This answer is conceptually the same as the one @josh gave, but presented as a more generic wrapper. Note: this version is for a 'writeable' computed.

I'm using Typescript so I've included the ts.d definition first. So ignore this first part if not relevant to you.

interface KnockoutStatic
{
    notifyingWritableComputed<T>(options: KnockoutComputedDefine<T>, context ?: any): KnockoutComputed<T>;
}


Notifying-writeable-computed

A wrapper for a writable observable that always causes subscribers to be notified - even if no observables were updated as a result of the write call

Just replace function<T> (options: KnockoutComputedDefine<T>, context) with function(options, context) if you don't use Typescript.

ko.notifyingWritableComputed = function<T> (options: KnockoutComputedDefine<T>, context)
{
    var _notifyTrigger = ko.observable(0);
    var originalRead = options.read;
    var originalWrite = options.write;

    // intercept 'read' function provided in options
    options.read = () =>
    {
        // read the dummy observable, which if updated will 
        // force subscribers to receive the new value
        _notifyTrigger();   
        return originalRead();
    };

    // intercept 'write' function
    options.write = (v) =>
    {
        // run logic provided by user
        originalWrite(v);

        // force reevaluation of the notifyingWritableComputed
        // after we have called the original write logic
        _notifyTrigger(_notifyTrigger() + 1);
    };

    // just create computed as normal with all the standard parameters
    return ko.computed(options, context);
}

The main use case for this is when you are updating something that would not otherwise trigger a change in an observable that is 'visited' by the read function.

For instance I am using LocalStorage to set some values, but there is no change to any observable to trigger re-evaluation.

hasUserClickedFooButton = ko.notifyingWritableComputed(
{
    read: () => 
    {
        return LocalStorageHelper.getBoolValue('hasUserClickedFooButton');
    },
    write: (v) => 
    {
        LocalStorageHelper.setBoolValue('hasUserClickedFooButton', v);        
    }
});

Note that all I needed to change was ko.computed to ko.notifyingWritableComputed and then everything takes care of itself.

When I call hasUserClickedFooButton(true) then the 'dummy' observable is incremented forcing any subscribers (and their subscribers) to get the new value when the value in LocalStorage is updated.

(Note: you may think the notify: 'always' extender is an option here - but that's something different).


There is an additional solution for a computed observable that is only readble:

ko.forcibleComputed = function(readFunc, context, options) {
    var trigger = ko.observable().extend({notify:'always'}),
        target = ko.computed(function() {
            trigger();
            return readFunc.call(context);
        }, null, options);
    target.evaluateImmediate = function() {
        trigger.valueHasMutated();
    };
    return target;
};


myValue.evaluateImmediate();

From @mbest comment https://github.com/knockout/knockout/issues/1019.



回答4:

suppose getCurrentValues() can return different sets of observables which are modified elsewhere in the code

I assume getCurrentValues() is a function. If you could make it a computed, your checkedValueCount would just magically start working.

Can you make getCurrentValues be a computed instead of a function?



回答5:

since there is no straight forward way to force update a computed, i have created an observable named toForceComputedUpdate, and i called it within the computed function so the computed will listen to its update, then to force update i call it like this toForceComputedUpdate(Math.random)