Javascript memory leak when changing model data in

2019-04-09 19:23发布

问题:

We're building a fairly large one-page-application with KnockoutJS as "data-handler".

The problem is that when changing model-data, the old models isn't disposed by the garbage collector (as it seems).

The application has about 12 different models with computed obserables that you can retreive relations with.

In the ViewModel I have a observable array for each of the model. Lets say I fill a array with 100 instances of a model. When I later want to change these 100 to 100 different instances the memory grows and it never goes down (I'm using Chrome and checking the in the Windows Task Manager).

The application is quite complex but I'll give some examples of what I'm doing (code is copied and simplified to only show some examples, some of the code might seem to be strange but its problably because I removed namespacing and other stuff).

ViewModel:

var ViewModel = (function () {
    var _departments = ko.observableArray(),
        _addresses = ko.observableArray();

    var UpdateData = function (data) {

        if (typeof data.departments != 'undefined') {
            var mappedData = ko.utils.arrayMap(data.departments, function(item) {
                return new Department(item);
            });
            _departments(mappedData);
        }

        if (typeof data.addresses != 'undefined') {
            var mappedData = ko.utils.arrayMap(data.addresses , function(item) {
                return new Address(item);
            });
            _addresses (mappedData );
        }
    };

    return {
        departments: _departments,
        addresses: _addresses,
        UpdateData: UpdateData
    };

})();

Department model

var Department = function (data) {

    var Department = {
        Id: ko.observable(data.ClusterId),
        Number: ko.observable(data.Number),
        Name: ko.observable(data.Name)
    };

    var Addresses = ko.computed(function () {
        return ko.utils.arrayFilter(ViewModel.addresses(), function (address) {
            return address.DepartmentId() === Department.Id();
        }).sort(function (a, b) {
            return a.Number() < b.Number() ? -1 : 1;
        });
    }, Department);

    var Update = function (data) {
        Department.Id(data.Id);
        Department.Number(data.Number);
        Department.Name(data.Name);
    };

    $.extend(Department, {
        Addresses: Addresses,
        Update: Update
    });

    return Department;

};

Address model

var Address = function (data) {

    var Address = {
        Id: ko.observable(data.Id),
        Number: ko.observable(data.Number),
        Text: ko.observable(data.Text),
        DepartmentId: ko.observable(data.DepartmentId)
    };

    var Department = ko.computed(function () {
        return ko.utils.arrayFirst(ViewModel.departments(), function (item) {
            return item.Id() == Address.DepartmentId();
        });
    }, Address);

    var Update = function (data) {
        Address.Id(data.Id);
        Address.Number(data.Number);
        Address.Text(data.Text);
        Address.DepartmentId(data.DepartmentId);
    };

    $.extend(Address, {
        Department: Department,
        Update: Update
    });

    return Address;

};

There are alot more code but what I'm looking for is a way to start finding the leak (I have tried to find it for some hours now). Is there something in my example that could cause it? Or has anyone else had this kind of problem? The app also has multiple custom bindings but I've tried to remove the HTML that uses the bindings and it seems that the "raw" javascript object is what taking up the memory.

If I'm changing the observable & computed variables in the models to functions that return a static value the objects seems to be disposed.

回答1:

The computed's within each Department and Address will keep their subscription to the viewmodel until you swap out both departments and addresses. For example, I would expect this to leak memory:

ViewModel.UpdateData({ departments: [ /* some new departments */ ] });
ViewModel.UpdateData({ departments: [ /* some new departments */ ] });
ViewModel.UpdateData({ departments: [ /* some new departments */ ] });
ViewModel.UpdateData({ departments: [ /* some new departments */ ] });
ViewModel.UpdateData({ departments: [ /* some new departments */ ] });
ViewModel.UpdateData({ departments: [ /* some new departments */ ] });
ViewModel.UpdateData({ departments: [ /* some new departments */ ] });
ViewModel.UpdateData({ departments: [ /* some new departments */ ] });

In this case, all of the "old" departments will still be bound to your ViewModel.Addresses. Changing out the Addresses should then release the memory:

ViewModel.UpdateData({ addresses: [ /* some new addresses */ ]});
// all the old departments should get GC'd now.

One way to resolve this is to modify your ViewModel to dispose of the old objects before removing them:

var ViewModel = (function () {
var _departments = ko.observableArray(),
    _addresses = ko.observableArray();

var UpdateData = function (data) {

    if (typeof data.departments != 'undefined') {
        var mappedData = ko.utils.arrayMap(data.departments, function(item) {
            return new Department(item);
        });

        // dispose of the computeds in the old departments
        ko.utils.arrayForEach(_departments(), function (d) { d.Addresses.dispose(); });
        _departments(mappedData);
    }

    if (typeof data.addresses != 'undefined') {
        var mappedData = ko.utils.arrayMap(data.addresses , function(item) {
            return new Address(item);
        });
        // dispose of the computeds in the old addresses
        ko.utils.arrayForEach(_addresses(), function (a) { a.Department.dispose(); });
        _addresses (mappedData );
    }
};

return {
    departments: _departments,
    addresses: _addresses,
    UpdateData: UpdateData
};

})();

Read the Knockout Computed Documentation. Specifically scroll to the bottom and read the documentation for dispose. Make sure you dispose of any computeds you create if they are being removed but the observables they are depending upon are not being removed.