سؤال

I show or hide a "Loading" indicator on my UI by binding its visibility to an observable named waiting, which is defined like this:

// Viewmodel
var outstandingRequests = ko.observable(0);

// true if any requests are outstanding
var waiting = ko.computed(function() {
    return outstandingRequests() > 0;
}.extend({ throttle: 500 });

// These are called when AJAX requests begin or end
function ajaxBegin() {
    outstandingRequests(++outstandingRequests());
}
function ajaxEnd() {
    outstandingRequests(--outstandingRequests());
}

<!-- View -->
<div data-bind="visible: waiting">Please wait, loading...</div>

I'm throttling the waiting observable because I don't want the loading message to appear unless the request is taking a long time (>500ms in this case), to increase the perceived speed of the application. The problem is that once a long-running request finishes, the loading indicator doesn't disappear until an additional 500ms has passed. Instead, when the last outstanding request finishes, I want waiting to flip to false immediately.

My first attempt at a fix involved using valueHasMutated(), but the update is still delayed.

function ajaxEnd() {
    outstandingRequests(--outstandingRequests());
    // If that was the last request, we want the loading widget to disappear NOW.
    outstandingRequests.valueHasMutated(); // Nope, 'waiting' still 500ms to update :(
}

How can I bypass the throttle extension and force waiting to update immediately?

هل كانت مفيدة؟

المحلول 2

What you really want is to delay the notifications from the waiting observable when it becomes true. This can be done by intercepting the notifySubscribers function of the observable:

var originalNotifySubscribers = this.isWaiting.notifySubscribers,
    timeoutInstance;
this.isWaiting.notifySubscribers = function(value, event) {
    clearTimeout(timeoutInstance);
    if ((event === 'change' || event === undefined) && value) {
        timeoutInstance = setTimeout(function() {
            originalNotifySubscribers.call(this, value, event);
        }.bind(this), 500);
    } else {
        originalNotifySubscribers.call(this, value, event);
    }
};

jsFiddle: http://jsfiddle.net/mbest/Pk6mH/

EDIT: I just thought of another, possibly better, solution for your particular case. Since the waiting observable only depends on one other observable, you can create a manual subscription that updates the waiting observable:

var timeoutInstance;
this.isLoading.subscribe(function(value) {
    clearTimeout(timeoutInstance);
    if (value) {
        timeoutInstance = setTimeout(function() {
            this.isWaiting(true);
        }.bind(this), 500);
    } else {
        this.isWaiting(false);
    }
}, this);

jsFiddle: http://jsfiddle.net/mbest/wCJHT/

نصائح أخرى

When you extend an observable, the extender typically wraps the observable with another with the desired behavior. You could keep a reference to the original observable which will allow you to make direct writes to it all while exposing your throttled version of the observable normally.

e.g.,

var myObservable = ko.observable('foo');
var myThrottledObservable = myObservable.extend({ throttle: 500 });
myThrottledObservable('bar'); // delayed
myObservable('baz'); // immediate

In your particular use case, rather than throttling the waiting observable, throttle the outstandingRequests observable and use the throttled value in waiting.

var outstandingRequests = ko.observable(0);

// throttled requests for the waiting observable
var throttledOutstandingRequests = outstandingRequests.extend({ throttle: 500 });

// true if any requests are outstanding
var waiting = ko.computed(function() {
    return throttledOutstandingRequests() > 0;
};

// These are called when AJAX requests begin or end
function ajaxBegin() {
    outstandingRequests(++outstandingRequests());
}
function ajaxEnd() {
    outstandingRequests(--outstandingRequests());
}

Writes to your outstandingRequests observable happen immediately but your waiting observable will effectively be throttled.


Alternatively, a cleaner solution in my opinion would be to reimplement the throttled extender to add the ability to update immediately.

ko.extenders['throttleEx'] = function(target, timeout) {
    // Throttling means two things:

    // (1) For dependent observables, we throttle *evaluations* so that, no matter how fast its dependencies
    //     notify updates, the target doesn't re-evaluate (and hence doesn't notify) faster than a certain rate
    target['throttleEvaluation'] = timeout;

    // (2) For writable targets (observables, or writable dependent observables), we throttle *writes*
    //     so the target cannot change value synchronously or faster than a certain rate
    var writeTimeoutInstance = null;
    var throttled = ko.dependentObservable({
        'read': target,
        'write': function(value) {
            clearTimeout(writeTimeoutInstance);
            writeTimeoutInstance = setTimeout(function() {
                target(value);
            }, timeout);
        }
    });

    // add function to set the value directly
    throttled['immediate'] = function(value) {
        target(value);
    };

    return throttled;
};

Then to use it:

var waiting = ko.computed(function() {
    return outstandingRequests() > 0;
}.extend({ throttleEx: 500 });

// These are called when AJAX requests begin or end
function ajaxBegin() {
    outstandingRequests.immediate(++outstandingRequests());
}
function ajaxEnd() {
    outstandingRequests.immediate(--outstandingRequests());
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top