ViewModel内存泄漏 - 摆脱循环依赖

时间:2013-09-14 00:31:27

标签: javascript knockout.js

我有一个生活在observableArray中的knockout.js子视图模型,这些模型依赖于父视图模型中的observable。父级observable绑定到select下拉菜单,孩子们需要知道该值何时发生变化。

我已经传递了整个父视图模型,只传递了observable,并且还在父级内部订阅了observable,以便“手动”更新子级。所有这些都用于绑定目的。

问题是,如果我然后清除孩子observableArray为新生儿腾出空间,或者只是用新生儿替换孩子,那些旧孩子会留在记忆中,因为他们仍然被反向引用依赖设置由knockout.js。

我愿意接受不同的设计模式或者告诉敲门声来停止依赖关系。在另一个例子中,我尝试了cleanNode(),但似乎没有清理这些反向依赖。

代码:

function FamilyViewModel(name) {
    this.name = name;
    this.children = ko.observableArray();
}

function ChildViewModel(firstName, lastName, selectedFamilyObservable) {
    this.firstName = ko.observable(firstName);
    this.lastName = ko.observable(lastName);
    this.fullName = ko.computed(function() {
        return this.firstName() + " " + this.lastName();
    }, this);
    this.isSelected = ko.computed(function() {
        return selectedFamilyObservable().name == this.lastName();
    }, this);
}

function PageViewModel() {
    this.families = ko.observableArray([
            new FamilyViewModel("Smith"),
            new FamilyViewModel("Jones"),
            new FamilyViewModel("Brown")
            ]);
    this.selectedFamily = ko.observable();
    this.addChild = _.bind(function() {
        this.selectedFamily().children.push(new ChildViewModel("Frank", this.selectedFamily().name, this.selectedFamily));
    }, this);
    this.resetChildren = _.bind(function() {
        this.selectedFamily().children([]);
    }, this);
}

$(function() {
    ko.applyBindings(new PageViewModel());
});

小提琴:

http://jsfiddle.net/cygnl7/xhzkC/1/

2 个答案:

答案 0 :(得分:2)

检查我的评论,因为我认为他们会帮助你一点。

我对你的小提琴进行了一些改动。我不太明白为什么你包括下划线,因为它不需要绑定事件。此外,您的childViewModel和familyViewModel实际上只是模型。系列中的isSelected属性只是一个额外的绑定,这意味着每次更改selectedFamily时,您必须外出并执行其他更新。最后,如果您只想重置selectedFamily的子项,只需删除ko.utils.arrayForEach函数,然后执行selectedFamily()。children([]);我不确定为什么你只想重置选定的家庭孩子,但这很容易做到。

需要代码 -

 var addChild = function () {
      self.selectedFamily().push(new childModel(self.selectedFamily().name());
 };

http://jsfiddle.net/xhzkC/2/

答案 1 :(得分:1)

前面提到的

Fiddle解决方案。

问题的关键在于,您不仅希望跟踪当前选定的系列(.selectedFamily),还希望每个系列都知道它们是否已被选中。我认为最简单的解决方案是订阅selectedFamily并在其更改之前及其更改后监视其值以设置系列的isSelected值。我喜欢这种方法,因为没有必要遍历族数组来执行此操作。

self.selectedFamily = ko.observable();
self.selectedFamily.subscribe(function (oldValue) {
    if (oldValue) {
        oldValue.isSelected(false);
    }
}, null, 'beforeChange');
self.selectedFamily.subscribe(function (newValue) {
    if (newValue) {
       newValue.isSelected(true);
    }
});

订阅默认功能在值已经更改后传递新值。在这里,您可以访问新选择的系列,并可以将其isSelected属性设置为true。但是订阅有两个内置的主题,默认情况下会被调用。第一个是我们刚刚讨论过的'变更'主题,另一个是'beforeChange'。这会在值更改之前调用,并使您有机会在值即将更改之前执行任何业务逻辑,例如将isSelected标志设置为false。

----------

请注意,下一部分只是我经历的一些注意事项和额外信息。以上是一个适当的解决方案,但对于那些想要了解更多信息的人,请继续阅读...

----------

另一种方法是使selectedFamily使用像这样的私有可观察支持计算读/写:

var _selectedFamily = ko.observable();
self.selectedFamily = ko.computed({
    read: _selectedFamily,
    write: function (newValue) {
        var oldValue = _selectedFamily();
        if (oldValue) {
            oldValue.isSelected(false);
        }
        if (newValue) {
           newValue.isSelected(true);
        }
        _selectedFamily(newValue);
    }
});

我如何进行此设置并非防弹,但应该让您了解这个概念。这不是一个糟糕的方法,如果我需要根据将决定设置计算结果的结果的当前值和下一个值做出决定,我会选择这样做。但在这种情况下,我不喜欢在这里设置isSelected标志,因为它与结果值无关。假设需要创建一个计算到容纳逻辑来确定设置的值的结果,我仍然会选择订阅解决方案,如前所述,但是在私有支持的_selectedFamily可观察对象中,在没有经过计算的情况下,需要设置私有的可观察值。


我在代码中解决的另一个问题是按钮上的点击绑定。我用with绑定把它们包起来。绑定到函数的绑定默认情况下将当前上下文的对象传递给它们。现在不必这样做:

self.addChild = function () {
    var selectedFamily = self.selectedFamily();
    // ...code...
};

你可以这样做:

self.addChild = function (selectedFamily) {
};

我还将<span data-bind="visible: isSelected">| Selected!</span>绑定移出了子环境并进入了家庭环境,因为我认为这是预期的。你有'|来似乎很奇怪isSelected'文本旁边的每个孩子而不是姓氏,但是,如果你想要每个孩子,你可以这样做:

<li data-bind="text: fullName"><span data-bind="visible: $parent.isSelected">| Selected!</span></li>

关于KO foreach绑定的最后一个特性是绑定一个具有数据/作为属性的对象。它可以使您的标记更具人性化,并且真正有助于访问父链。假设我们确实希望使用家庭的isSelected标志来显示孩子们的名字。以上我们必须使用$parent来引用。在某些情况下,您有多个嵌套的foreach绑定,您可能会发现自己编写<span data-bind="text: $parents[2].name"></span>之类的内容,这些内容几乎没有提供有关您尝试引用的上下文的信息。使用此方法,您可以改为:

<div data-bind="foreach: { data: $root.families, as: 'family' }">
    <ul data-bind="foreach: { data: $family.children, as: 'child }>
        <li>
            <span data-bind="text: child.firstName">
            <span data-bind="text: family.name">
        </li>
    </ul>
</div>

通过这种方式,您甚至不需要计算机来写出first和lastName。当我认为我将要有一个复杂的嵌套上下文绑定结构和/或当我想使一个简单的嵌套结构看起来更具人性化时,我会使用这种方法。

http://jsfiddle.net/34ZjB/1/