Another way of phrasing the same thoughts, in a more general form:
In practice there are sometimes uses for standalone or "orphan" computeds, where they return nothing but have side-effects:
ko.computed(function() {
var v = someOtherObservable();
// have some side-effect using v
});
In any view model code that is setup and torn down repeatedly, the above example is woefully incomplete because it doesn't get disposed.
It would be ideal if such an orphan could be associated with an "owner", which would be an observable supporting awake/asleep events. This would take care of building/disposing the actual computed based on the wakefulness of the owner:
var xo = {}; // for knockout extensions
xo.execute = function(owner, evaluator, thisObj) {
var orphan, extenders = [];
owner.subscribe(function() {
if (!orphan) {
orphan = ko.computed(evaluator, thisObj);
extenders.forEach(function(e) {
orphan.extend(e);
});
}
}, null, "awake");
owner.subscribe(function() {
if (orphan) {
orphan.dispose();
orphan = null;
}
}, null, "asleep");
return {
extend: function(extender) {
if (orphan) {
orphan.extend(extender);
}
extenders.push(extender);
}
};
};
(NB. not sure about the extenders part but I guess something like that is needed).
The idea is that the side-effects (whatever they are) would only be needed to occur when the owner is awake. When the owner is asleep, the orphan computed is internally torn down.
The only caveat is that ordinary observables don't emit asleep/awake events, so only pureComputeds can serve as owners for others.
It would be great if asleep/awake events could be integrated into plain observables, but obviously that adds complexity to the most heavily used feature of KO so may not be desirable. If so, it can be defined by another external wrapper:
xo.observable = function(initVal) {
var observable = ko.observable(initVal);
var changeSubscriptionCount = 0;
observable.beforeSubscriptionAdd = function (event) {
if (event === 'change') {
if (changeSubscriptionCount++ === 0) {
observable.notifySubscribers(observable.peek(), "awake");
}
}
};
observable.afterSubscriptionRemove = function (event) {
if (event === 'change') {
if (--changeSubscriptionCount === 0) {
observable.notifySubscribers(observable.peek(), "asleep");
}
}
};
return observable;
};
Out of these we get a "leak-proof" pattern for using KO:
- Use xo.execute instead of standalone or "orphaned" ko.computed
- Use xo.observable instead of ko.observable if you want it to serve as owner of an xo.execute
- Everywhere else, use ko,pureComputed. Never use a raw ko.computed
- The promiseComputed I was originally asking about can be implemented easily on top of xo.execute/xo.observable. It is very nearly the original code
This way, you are safe from memory retention or weird side-effects that keep happening after their associated view-model has been abandoned, and you don't have to manually dispose anything.
Does this make sense?
Also, is there a way to ask a Knockout subscribable what events it can emit? That would allow xo.execute to valid the owner passed to it, rather than subscribing to awake and never hearing anything. I wondered about the possibility of making subscribables throw an exception if you try to subscribe to an event they don't ever emit, but of course that would be a breaking change.