I have a single-page app where the user pages through lists of items. Each item, in turn, has a list of items.
An observable array is updated with new items from the server retrieved via an AJAX request. This all works fine.
Unfortunately after a few pages, the number of operations performed (and the amount of memory used in browsers like FireFox and IE8) keeps going up. I've tracked it down to the fact that elements in my observable array are not being cleaned up properly and are actually still in memory, even though I've replaced the items in my observable array with new data.
I've created a small example that replicates the problem I'm seeing:
HTML:
<p data-bind="text: timesComputed"></p>
<button data-bind="click: more">MORE</button>
<ul data-bind="template: { name: 'items-template', foreach: items }">
</ul>
<script id="items-template">
<li>
<p data-bind="text: text"></p>
<ul data-bind="template: { name: 'subitems-template', foreach: subItems }"></ul>
</li>
</script>
<script id="subitems-template">
<li>
<p data-bind="text: text"></p>
</li>
</script>
JavaScript/KnockoutJS ViewModels:
var subItemIndex = 0;
$("#clear").on("click", function () {
$("#log").empty();
});
function log(msg) {
$("#log").text(function (_, current) {
return current + "\n" + msg;
});
}
function Item(num, root) {
var idx = 0;
this.text = ko.observable("Item " + num);
this.subItems = ko.observableArray([]);
this.addSubItem = function () {
this.subItems.push(new SubItem(++subItemIndex, root));
}.bind(this);
this.addSubItem();
this.addSubItem();
this.addSubItem();
}
function SubItem(num, root) {
this.text = ko.observable("SubItem " + num);
this.computed = ko.computed(function () {
log("computing for " + this.text());
return root.text();
}, this);
this.computed.subscribe(function () {
root.timesComputed(root.timesComputed() + 1);
}, this);
}
function Root() {
var i = 0;
this.items = ko.observableArray([]);
this.addItem = function () {
this.items.push(new Item(++i, this));
}.bind(this);
this.text = ko.observable("More clicked: ");
this.timesComputed = ko.observable(0);
this.more = function () {
this.items.removeAll();
this.addItem();
this.addItem();
this.addItem();
this.timesComputed(0);
this.text("More clicked " + i);
}.bind(this);
this.more();
}
var vm = new Root();
ko.applyBindings(vm);
If you look at the fiddle, you will notice that the "log" contains an entry for every single ViewModel ever created. the computed property SubItem.computed
is run even after I expected each of those items to be long gone. This is causing a serious degradation in performance in my application.
So my questions are:
ko.computed
on SubItem
causing the issue?Update: After some further digging, I'm pretty sure the computed property in SubItem
is the culprit. However, I still don't understand why that property is still being evaluated. Shouldn't SubItem
be destroyed when the observable array is updated?
The JavaScript garbage collector can only dispose a computed observable once all references to it and its dependencies are dropped. That's because observables keep a reference to any computed observables that depend on them (and vice versa).
One solution is to make the computed observable dispose itself when it no longer has any dependencies. This can be done easily using a helper function like this.
function autoDisposeComputed(readFunc) {
var computed = ko.computed({
read: readFunc,
deferEvaluation: true,
disposeWhen: function() {
return !computed.getSubscriptionsCount();
}
});
return computed;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With