【问题标题】:Strange memory leak in knockout mapping plugin淘汰赛映射插件中的奇怪内存泄漏
【发布时间】:2026-01-10 15:20:06
【问题描述】:

如果视图模型是使用 knockout.mapping 插件创建的,则无法弄清楚为什么处理计算的 observable 不会从全局变量中删除订阅。
首先让我们看看直接创建模型时会发生什么:

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

我在多次添加和删除项目时使用 Chrome 开发工具记录堆分配。每次添加后,之前分配的对象都被清理成功,得到如下图:

现在使用映射插件具有相同的功能:

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

使用相同的技术记录堆分配这是我所看到的:

我知道pureComputed,但出于两个原因想避免使用它们:

  1. 切换到纯计算会通过抛出异常来破坏遗留代码:

    '纯'计算不能被递归调用

解决这些问题需要很长时间。

  1. 更频繁地评估纯计算,这会产生一些我想避免的性能开销,并且这再次对遗留代码产生不可预测的影响。

此外,我仍然想使用映射插件,因为它能够监控集合状态(使用 key 映射属性)并且因为它为我创建了所有可观察对象。

那么有什么我错过的吗?在使用映射插件的情况下释放资源的正确方法是什么?

【问题讨论】:

    标签: javascript knockout.js knockout-mapping-plugin


    【解决方案1】:

    深入研究映射插件,它对计算进行了一些黑客攻击,在这种情况下显然会破坏它。

    看起来将计算出的价格设置为deferEvaluation 会使映射插件基本上不用管它。

    this.price = ko.computed(function () {
        // Computed is subscribed to global variable.
        return price + ' ' + Environment.currencyStr();
    }, this, { deferEvaluation: true });
    

    【讨论】:

    • 哇,这太出乎意料了。我明天试一试,告诉你结果,谢谢!
    • 等不及了。泄漏似乎真的消失了。您能否在映射插件中指出您发现计算行为中断的行号(或要查找的确切代码行)?再次感谢!
    • 我无法确定它崩溃的确切原因,但代码在这里github.com/SteveSanderson/knockout.mapping/blob/master/… - 它似乎试图将所有计算设置为 deferEvaluation 因为它们所依赖的属性可能被加载到映射插件的顺序错误,因此第一次评估可能会失败。
    • 我想我明白了主要想法 - 他们想为我的视图模型中找到的所有计算创建代理(出于您提到的原因)。所以当我尝试处理我的计算时,没关系,因为代理被处理了,而不是真正的计算(至少我是这么认为的)。但是当我们设置 deferEvaluation: true 时,他们不会创建代理,所以一切都按预期工作。谢谢,这是一个很好的调查!