Knockout subscription insight: detecting if disposed

3.8k views Asked by At

I have a subscription management in my ko model, which saves any subscription by intercepting them and saving reference to them.

I then dispose() them, but I need to understand sometimes if a sub is already disposed. Is the member Gb, a boolean of the subscription object, an active-inactive flag? I see that sometimes when I dispose a subscription its Gb becomes false, sometimes not. Do I have to interpret this as a dispose fail?

And once I dispose a subscription, the subscription object, is ready to be garbage collected?

EDIT: I just need to understand if a subscription is already disposed, as said in the title. I obtain the subscription in the only way I know, saving a reference to it when declared:

var mySub = myObservable.subscribe(function(){/*do something*/});

Some time after I need a way to determine if mySub is already disposed.

function isDisposed(mySubscription) {
    // if is disposed return true else return false
}

I need to perform some logic based on this, not only dispose it if it is not disposed already (or I could simply call again the dispose method). Is it possible to determine subsbcription disposal status?

2

There are 2 answers

4
Olga On BEST ANSWER

About manual subscriptions

Knockout source is compiled with Google Closure Compiler so only properties and methods explicetly exported in source appear in compiled library code.

With that said a subscription has a "private" property isDisposed, but it is not exported. Thus the only API exported for subscription is dispose.

Glimps of source code - ko.subscription (knockout-3.1.0.debug):

ko.subscription = function (target, callback, disposeCallback) {
    this.target = target;
    this.callback = callback;
    this.disposeCallback = disposeCallback;
    this.isDisposed = false;
    ko.exportProperty(this, 'dispose', this.dispose);
};
ko.subscription.prototype.dispose = function () {
    this.isDisposed = true;
    this.disposeCallback();
};

About memory leaks and computeds

1) Just an interesting fact about computeds - consider the following computed

var myObservable1 = ko.observable(true);
var myObservable2 = ko.observable('foo');

var myComputed = ko.computed(function () {
    if (myObservable1()) {
        return myObservable2() + 'bar';
    }
});

In this example myComputed has 2 dependecies. But if we were to assing false to myObservable1 myComputed would reevaluate and after reevalution it would only have 1 dependency and subscription to myObservable2 would be disposed.

Why it is disposed:

The explanation lies in how computed is evaluated - it registers dependencies the following way - if any observable is read (meaning code like myObservable1()) during evaluation - computed recieves a callback with this observable, checks its id and stores it in a new dependency array. Once evaluation is complete - old dependency array is disposed. In our example when myObservable1 is set to false myObservable2 is never read - since we never enter if block. So it is not a new dependecy and old dependency is disposed.

2) Another interesting fact. Consider snippet:

(function () {
    var myObservable = ko.observable(0);
    var myModel = {
        myComputed: ko.computed(function () {
            console.log(myObservable());
        })
    };
    myModel = undefined;
    myObservable(42); // Outputs 42
})();

The computed is not collected by garbage collector because infact the reference to it exists inside it's dependency.

Glimps of source code - ko.computed (knockout-3.1.0.debug):

function addSubscriptionToDependency(subscribable, id) {
    if (!_subscriptionsToDependencies[id]) {
        _subscriptionsToDependencies[id] = subscribable.subscribe(evaluatePossiblyAsync);
        ++_dependenciesCount;
    }
}
...
function evaluatePossiblyAsync() {
    var throttleEvaluationTimeout = dependentObservable['throttleEvaluation'];
    if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) {
        clearTimeout(evaluationTimeoutInstance);
        evaluationTimeoutInstance = setTimeout(evaluateImmediate, throttleEvaluationTimeout);
    } else if (dependentObservable._evalRateLimited) {
        dependentObservable._evalRateLimited();
    } else {
        evaluateImmediate();
    }
}

Reference to dependentObservable is preserved because reference to evaluatePossiblyAsync is preserved (Closures, JS and all that Jazz).

Whoa, that is all I had to say. Hope some thought come to mind.

0
Brian M. Hunt On

One definitive way to determine if an active computed has been disposed is to see how many subscriptions an observable has.

Here, have a look:

>>> y = ko.observable()
>>> y.getSubscriptionsCount()
0
>>> x = ko.computed(function () { y() })
>>> y.getSubscriptionsCount()
1
>>> x.dispose()
>>> y.getSubscriptionsCount()
0

Be mindful that with pure computeds the getSubscriptionsCount will be 0 when the pure compute is asleep i.e. has no subscriptions itself. i.e.

>>> x = ko.pureComputed(function () { y() })
>>> y.getSubscriptionsCount()
0
>>> z = x.subscribe(console.log.bind(console))
>>> y.getSubscriptionsCount()
1

In other words pure computeds may appear to be disposed of when they are merely sleeping.

You could wake up a pure computed by subscribing to it; if the subscription count of the dependency is still zero, it must be disposed.