6

我正在尝试在 Backbone.js 中实现我的“实例存储”版本,如 Soundcloud 在他们最近的博客文章中所述:

http://backstage.soundcloud.com/2012/06/building-the-next-soundcloud/

相关摘录:

为了解决这个问题,我们使用了一个我们称为实例存储的构造。此存储是一个对象,每次调用模型的构造函数时都会隐式访问和修改它。当第一次构建模型时,它会将自己注入到 store 中,使用它的 id 作为唯一键。如果使用相同的 id 调用相同的模型构造函数,则返回原始实例。

var s1 = new Sound({id: 123}),
    s2 = new Sound({id: 123});

s1 === s2; // true, these are the exact same object.

这是因为 Javascript 的一个鲜为人知的特性。如果构造函数返回一个对象,那么这就是分配的值。因此,如果我们返回对先前创建的实例的引用,我们将获得所需的行为。在幕后,构造函数基本上是这样做的:

var store = {};

function Sound(attributes) {
    var id = attributes.id;

    // check if this model has already been created
    if (store[id]) {
        // if yes, return that
        return store[id];
    }
    // otherwise, store this instance
    store[id] = this;
}

我通过重写 Backbone.Model 类来创建我自己的构造函数来实现我的版本。

var MyModel = Backbone.Model.extend({
    constructor: function (attributes, options) {
        var id = attributes ? attributes.id : undefined;

        if (this.store[id]) {
            return this.store[id];
        }

        Backbone.Model.prototype.constructor.apply(this, arguments);

        if (id) {
            this.store[id] = this;
        }
    }
});

var MyOtherModel = MyModel.extend({
    store: {},

    //other model stuff
});

这工作得很好,但一定发生了一些变化,现在它停止工作了,我不确定为什么。新创建的实例毫无问题地存储在存储对象中 - 每个扩展 MyModel 类的类都有自己的空存储,以避免具有相同 id 的不同类型的实例发生冲突。当使用现有 id 调用构造函数时,也可以毫无问题地检索正确的实例,但是当它们从构造函数返回时,返回值将被忽略。我对规范的理解是,构造函数可以返回一个对象——但不能返回一个原始对象——并且当使用 new 运算符调用构造函数时,返回的对象将被分配到赋值语句的左侧。这不会发生,即使构造函数返回一个对象,

一些调试信息。不确定这些信息会有多大帮助。这是第一次实例化对象的 MyModel 构造函数中的“this”。

child
    _callbacks: Object
    _escapedAttributes: Object
    _previousAttributes: Object
    _setting: false
    attributes: Object
        id: "4fd6140032a6e522f10009ac"
        manufacturer_id: "4f4135ae32a6e52a53000001"
        name: "Tide"
        uniqueName: "tide"
    __proto__: Object
    cid: "c50"
    collection: child
    id: "4fd6140032a6e522f10009ac"
    __proto__: ctor
        constructor: function (){ parent.apply(this, arguments); }
        defaults: Object
        store: Object
        url: function () {
        urlRoot: function () {
        __proto__: ctor

当它是从实例存储返回的对象时,这是 MyModel 构造函数中的“this”:

child
    _callbacks: Object
    _escapedAttributes: Object
    _previousAttributes: Object
    _setting: false
    attributes: Object
        _validate: function (attrs, options) {
        bind: function (events, callback, context) {
        change: function (options) {
        changedAttributes: function (diff) {
        clear: function (options) {
        clone: function () {
        constructor: function (){ parent.apply(this, arguments); }
        defaults: Object
        destroy: function (options) {
        escape: function (attr) {
        fetch: function (options) {
        get: function (attr) {
        has: function (attr) {
        hasChanged: function (attr) {
        idAttribute: "id"
        initialize: function (){}
        isNew: function () {
        isValid: function () {
        manufacturer_id: 0
        name: ""
        off: function (events, callback, context) {
        on: function (events, callback, context) {
        parse: function (resp, xhr) {
        previous: function (attr) {
        previousAttributes: function () {
        save: function (key, value, options) {
        set: function (key, value, options) {
        store: Object
        toJSON: function () {
        trigger: function (events) {
        unbind: function (events, callback, context) {
        unset: function (attr, options) {
        url: function () {
        urlRoot: function () {
        __proto__: Object
        cid: "c141"
     __proto__: ctor
        constructor: function (){ parent.apply(this, arguments); }
        defaults: Object
        store: Object
        url: function () {
        urlRoot: function () {
        __proto__: ctor

我注意到的是,第二个中的属性对象包含其中包含的主干对象的所有方法,它们不应该是。它也没有id,我也不知道为什么。希望这能提供一些见解。谢谢。

4

3 回答 3

3

我不会为此使用扩展,我认为拥有一个单独的“工厂”是正确的想法。它将允许您扩展模型而不必担心副作用。

从带注释的源主干中,扩展了一些奇怪的东西,我还没有完全理解它。(也检查继承)所以让我们现在跳过它并坚持你的工作解决方案。

我已经修改了您生成工厂模型的方法,您应该能够像普通模型一样使用它们(例如,将它们设置在集合上),除非扩展它们不起作用。他们还将处理使用新数据更新您的模型,就像 soundcloud 示例一样。

var makeStoreable = function(model){
  var StoreModel = function(attr, opt){
    if(!attr || !attr.id){
      // The behavior you exhibit here is up to you
      throw new Error('Cool Models always have IDs!');
    }
    if(this.store[attr.id]){
      this.store[attr.id].set(attr, opt);
    }else{
      var newModel = new model(attr, opt);
      this.store[attr.id] = newModel;
    }
    return this.store[attr.id];
  };
  StoreModel.prototype.store = {};
  return StoreModel;
};

var CoolModel = Backbone.Model.extend({});

CoolModel = makeStoreable(CoolModel);

var a = new CoolModel({
    id: 4,
    coolFactor: 'LOW'
});

var b = new CoolModel({
    id:4,
    coolFactor: 'HIGH'
});

console.log(a===b); //true!
console.log(a.get('coolFactor') === 'HIGH'); //true!

这是一个可以玩的小提琴。

另外,我欢迎有人提出一个模型解决方案,将“存储”保留在模型实例的原型中。同样为了防止内存泄漏,我们可能应该在工厂或模型本身上创建一个引用计数销毁方法。

于 2012-06-22T16:32:11.200 回答
2

@wizard 的方法看起来非常漂亮和干净。对此+1。

它在 SoundCloud 中的实现方式是重写Backbone.Model.extend方法以使用我们修改的构造函数和闭包中的存储来创建一个类。store 最初是在一个闭包中创建的,以保持类的接口干净,但一段时间后发现它对调试很有用,可以引用每个类的 store,所以它也附在了那里。

我们确实有一个引用计数,因此内存使用量不会爆炸,并且还让类能够定义一个自定义函数,该函数给出唯一值来识别它。大部分时间id就足够了,但在某些极端情况下它并不完全奏效。

我欢迎有人提出一个模型内解决方案,将“存储”保留在模型实例的原型中

你可以做myInstance.constructor.store

于 2012-06-22T18:00:19.370 回答
0

使用@reconbot 解决方案后,我发现它破坏了 instanceof 运算符:

(new CoolModel) instanceof CoolModel // FALSE!!!

var MyModel = Backbone.Model.extend({
    idAttribute: 'myId'
});
new MyModel({ myId: 1 }) === new MyModel({ myId: 1 }) // FALSE!

我开发了一个新版本,它使用模型自己的 id 属性(通过 idAttribute)并使用 instanceof 并允许您扩展工厂:

小提琴

function makeStoreable(model) {
    var store = {};
    var idField = model.prototype.idAttribute;

    function ModelFactory(attr, opt) {
        if (!attr || !(idField in attr)) {
            throw new Error('Cool Models always have IDs!');
        }

        var id = attr[idField];

        if (store.hasOwnProperty(id)) {
            store[id].set(attr, opt);
        } else {
            model.call(this, attr, opt);
            store[id] = this;
        }

        return store[id];
    }

    function intermediate() {}
    intermediate.prototype = model.prototype;
    ModelFactory.prototype = new intermediate;

    // Only EcmaScript5!
    // ModelFactory.extend = model.extend.bind(model);
    ModelFactory.extend = function() {
        return model.extend.apply(model, arguments);
    };

    return ModelFactory;
}

和测试:

var RareID = Backbone.Model.extend({
    idAttribute: '_myOwnServerId'
});
RareID = makeStoreable(RareID);

var a = new RareID({
    _myOwnServerId: 4,
    coolFactor: 'LOW'
});

var b = new RareID({
    _myOwnServerId: 4,
    coolFactor: 'HIGH'
});

console.log(a===b); //true!
console.log(a instanceof RareID); //true!
console.log(a.get('coolFactor') === 'HIGH'); //true!

:)

于 2012-12-20T10:34:21.857 回答