ViewModel内存泄漏-摆脱循环依赖 [英] ViewModel Memory Leak - Getting Rid of a Circular Dependency

查看:803
本文介绍了ViewModel内存泄漏-摆脱循环依赖的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在依赖于其父视图模型中的observableobservableArray中有心剔除(knockout.js)子视图模型.父母的observable绑定到select下拉列表,孩子需要知道该值何时更改.

我已经传递了整个父视图模型,只传递了observable,并且还为observable订阅了父内部视图,以便手动"更新子级.所有这些都是出于约束目的.

问题是,如果我随后清除子级observableArray以便为新子级腾出空间,或者只是用新子级替换这些子级,则旧的子项会留在内存中,因为它们仍被反向依赖集引用由敲出.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/

解决方案

小提琴解决方案. >

问题的症结在于,您不仅要跟踪当前选择的家庭(.selectedFamily),而且还希望每个家庭都知道他们是否被选中.我认为最简单的解决方案是订阅selectedFamily并在其更改之前和更改之后监视其值以设置家庭的isSelected值.我喜欢这种方法,因为这样做不需要遍历Family数组.

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.但是订阅有两个内置的 topic ,默认情况下会被调用.第一个是我们刚刚讨论的更改"主题,另一个是"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绑定的最后一个功能是与具有data/as属性的对象绑定.它可以使您的标记更具可读性,并确实有助于访问父链.假设我们确实想使用家庭的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>

通过这种方式,您甚至都不需要计算出名字和姓氏.当我认为我将拥有一个复杂的嵌套上下文绑定结构时,和/或当我想使一个简单的嵌套结构看起来更易于阅读时,我将使用此方法.

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

I have knockout.js child view models living in an observableArray that depend on an observable in their parent view model. The parent's observable is bound to a select drop-down, and the children need to know when that value changes.

I've passed the entire parent view model, just the observable, and also subscribed inside the parent to the observable in order to update the children "manually." All of these work for binding purposes.

The problem is if I then clear out the child observableArray to make room for new children, or simply replace the children with new children, the old children stick around in memory because they're still referenced by the reverse dependency set up by knockout.js.

I am open to either a different design pattern or a way to tell knockout to stop holding onto the dependencies. In another example I tried cleanNode() but that didn't seem to clean up these reverse dependencies.

Code:

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());
});

Fiddle:

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

解决方案

Fiddle solution referenced earlier.

The crux of your problem is that you want to not only keep track of the currently selected family (.selectedFamily) but that you also want each family to know if they're selected. I think the simplest solution is to subscribe to selectedFamily and monitor its value before it changes and after it changes to set a family's isSelected value. I like this approach because there's no need to loop through the families array in order to do this.

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);
    }
});

Subscriptions default functionality passes in the new value after the value has already changed. Here you'll have access to the newly selected family and you can set its isSelected property to true. But subscriptions have two built in topics that get called by default. The first is the 'change' topic, that we just discussed, and the other is 'beforeChange'. This gets called before the value changes and gives you an opportunity to perform any business logic before a value is about to change, like setting an isSelected flag to false.

----------

Note, this next part is just some considerations I went through and extra information. The above is an adequate solution but for those who want to know more, read on...

----------

Another approach is to make selectedFamily a read/write computed with a private observable backing like this:

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);
    }
});

How I have this set up isn't bullet proof but should give you an idea of the concept here. This isn't a bad approach, and is one I'd go with if I had a need to make a decision based on the current and next value that would determine the outcome of setting the computed's result. But in this situation, I'm not a fan of setting the isSelected flag in here because it has no bearing on the resulting value. Assuming there was a need to create a computed to house logic that determined the outcome of the value that gets set, I would still opt with the subscriptions solution, as previously described, but on the privately backed _selectedFamily observable, in the off chance that there's a need to set the privately back observable's value without going through the computed.


The other piece I've addressed in the code is the click bindings on the buttons. I wrapped them in a with binding. Bindings that are bound to functions have the object of the current context passed into them by default. Now instead of having to do this:

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

You can do:

self.addChild = function (selectedFamily) {
};


I've also moved the <span data-bind="visible: isSelected">| Selected!</span> binding out of the children context and into the family context as I think this is what was intended. It seemed weird that you'd have the '| isSelected' text beside each child rather than the family name, but, if you wanted it by each child, you could do:

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


One last feature about the KO foreach binding is binding with an object that has data/as properties. It can make your markup more human readable and really helps with accessing parent chains. Let's say we did want to use the family's isSelected flag to display something by the children's names. Above we have to reference using $parent. In some situations, you have multiple nested foreach bindings and you may find yourself writing things like <span data-bind="text: $parents[2].name"></span> which gives little information about the context you're trying to reference. Using this method, you could instead write:

<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>

In this way, you wouldn't even need a computed to write out the first and lastName. I would use this method when I think I'm going to have a complex nested context binding structure and/or when I want to make a simple nested structure seem more human readable.

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

这篇关于ViewModel内存泄漏-摆脱循环依赖的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆