Search code examples
knockout.jssubscriptionssinglepage

Knockout subscription insight: detecting if disposed


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?


Solution

  • 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.