最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How should I be cleaning up KnockoutJS ViewModels? - Stack Overflow

programmeradmin8浏览0评论

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);
  thisputed = koputed(function () {
    log("puting for " + this.text());
    return root.text();
  }, this);

  thisputed.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 puted property SubItemputed 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:

  • What am I doing wrong here? Am I expecting KnockoutJS to dispose of ViewModels that I actually need to be disposing of manually?
  • Is my use of koputed on SubItem causing the issue?
  • If KnockoutJS is not going to dispose of these viewmodels, how should I be disposing of them myself?

Update: After some further digging, I'm pretty sure the puted 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?

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.puted = ko.puted(function () {
    log("puting for " + this.text());
    return root.text();
  }, this);

  this.puted.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 puted property SubItem.puted 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:

  • What am I doing wrong here? Am I expecting KnockoutJS to dispose of ViewModels that I actually need to be disposing of manually?
  • Is my use of ko.puted on SubItem causing the issue?
  • If KnockoutJS is not going to dispose of these viewmodels, how should I be disposing of them myself?

Update: After some further digging, I'm pretty sure the puted 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?

Share Improve this question edited Jan 17, 2013 at 23:01 Andrew Whitaker asked Jan 17, 2013 at 22:31 Andrew WhitakerAndrew Whitaker 126k32 gold badges295 silver badges308 bronze badges 3
  • Sorry, didn't get to dig into this really, but observables store their list of dependencies, so the root.text has a reference to the SubItem's puted. You can call .dispose() on a puted. Also, you can pass in a disposeWhen function to a puted that gets executed every time that it is evaluated, but in your case it would need to access to its parent and root to determine if the parent had already been removed from the root's observableArray. Probably better to proactively dispose. – RP Niemeyer Commented Jan 17, 2013 at 23:33
  • Thanks for the tip and confirmation that it's the puted property--I know you can dispose of puteds in both of those ways, but it would be kind of hard for me to wire up a sensible disposeWhen function. As for calling dispose, I would have to find every single instance of the puted property and manually call dispose, which seems pretty cumbersome. It might be worth re-working my application to just not do this. – Andrew Whitaker Commented Jan 17, 2013 at 23:37
  • I agree that it is probably better to find a way not to structure it in this way. The puted will always have a dependency on root.text. If you needed to do it, then I would probably put a dispose function on your Item that loops through the SubItems and disposes the puted. – RP Niemeyer Commented Jan 18, 2013 at 2:21
Add a ment  | 

1 Answer 1

Reset to default 8

The JavaScript garbage collector can only dispose a puted observable once all references to it and its dependencies are dropped. That's because observables keep a reference to any puted observables that depend on them (and vice versa).

One solution is to make the puted 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 puted = ko.puted({
        read: readFunc,
        deferEvaluation: true,
        disposeWhen: function() {
            return !puted.getSubscriptionsCount();
        }
    });
    return puted;
}
发布评论

评论列表(0)

  1. 暂无评论