Can't figure out why disposing computed observables doesn't remove the subscriptions from global variables in case view model was created using knockout.mapping plugin.
First let's see what happens when model is created directly:
// Global variable.
var Environment = {
currencyStr: ko.observable("usd.")
};
// Item model, used intensively.
function ItemModel(price) {
var self = this;
this.price = ko.computed(function () {
// Computed is subscribed to global variable.
return price + ' ' + Environment.currencyStr();
});
};
ItemModel.prototype.dispose = function () {
// Dispoing subscription to global variable.
this.price.dispose();
};
function ViewModel() {
var self = this;
self.items = ko.observableArray([]);
// Simply adds 1000 new items to observable array directly.
self.addItems = function () {
for (var i = 0; i < 1000; i++) {
self.items.push(new ItemModel(i));
}
};
// Disposes and removes items from observable array
this.removeItems = function () {
ko.utils.arrayForEach(self.items(), function (item) {
item.dispose();
});
self.items.removeAll();
};
};
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<button data-bind="click: addItems">Add items</button>
<button data-bind="click: removeItems">Remove items</button>
<div data-bind="foreach: items">
<div>
<span data-bind="text: price"></span>
</div>
</div>
I used Chrome dev tools to record heap allocations while adding and removing items several times. After each addition, previously allocated objects were cleaned up successfully, I got the following picture:
Now the same functionality using mapping plugin:
// Global variable.
var Environment = {
currencyStr: ko.observable("usd.")
};
// Item model, used intensively.
function ItemModel(price) {
var self = this;
this.price = ko.computed(function () {
// Computed is subscribed to global variable.
return price + ' ' + Environment.currencyStr();
});
};
ItemModel.prototype.dispose = function () {
// Dispoing subscription to global variable.
this.price.dispose();
};
function ViewModel() {
var self = this;
self.items = ko.observableArray([]);
self.itemsMapping = {
'create': function (options) {
return new ItemModel(options.data);
}
};
// Simply adds 1000 new items to observable array using mapping plugin.
self.addItems = function () {
var itemsPrices = new Array(1000);
for (var i = 0; i < 1000; i++) {
itemsPrices[i] = i;
}
// Mapping new array to our observable array.
ko.mapping.fromJS(itemsPrices, self.itemsMapping, self.items);
};
// Disposes and removes items from observable array
this.removeItems = function () {
ko.utils.arrayForEach(self.items(), function (item) {
item.dispose();
});
self.items.removeAll();
};
};
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js"></script>
<button data-bind="click: addItems">Add items</button>
<button data-bind="click: removeItems">Remove items</button>
<div data-bind="foreach: items">
<div>
<span data-bind="text: price"></span>
</div>
</div>
Using the same technique to record heap allocations this is what I see:
I am aware of pureComputed, but would like to avoid using them for 2 reasons:
- Switching to pure computed breaks legacy code by throwing exceptions:
'A 'pure' computed must not be called recursively
Solving these issues will take a lot of time.
- Pure computeds are evaluated more often which creates some performance overhead I'd like to avoid, and again this influences legacy code unpredictably.
Also I would still like to use the mapping plugin because of it's ability to monitor collection state (using key
mapping property) and because it creates all observables for me.
So is there something that I missed and what is the proper way to free resources in case of using mapping plugin?