问题
I'm trying to build a model that dynamically updates Session variables in a Meteor project. I know that plain JSON should not be stored within backbone models, so I have a Special model set up like so:
initialize : function () {
// Log the changed properties
this.on('change', function (model, options) {
for ( var i in options.changes)
this.display(i);
Session.set('NewSpecial', model);
});
},
//Attributes
defaults: {
"Product" : null,
"ShortDescription" : null,
"Category" : "food",
"Price" : new PriceModel,
"Date" : new DateModel,
"Uses" : 0,
"Tags" : [],
"Contributor" : null
},
With "Price" and "Date" being stored in their own models:
//Price model for use within Special
var PriceModel = Backbone.Model.extend({
defaults : {
"Regular" : null,
"Special" : null,
"PercentOff" : null
}
});
//Date model for use within Special
var DateModel = Backbone.Model.extend({
defaults : {
"StartTime" : null,
"EndTime" : null,
"HumanTimeRange" : null
}
});
As shown, when the attributes of the Special model change, it should call display for the attribute that changed, and then set the Session var to the model. If my DateModel or PriceModel change however, it doesn't appear to trigger a change event on the Special model. Should each "DateModel" and "PriceModel" have their own this.on('change', ...)
methods that call Special.set(attribute, thisModel)
methods? Or is there a different way to go about this?
回答1:
I see a couple problems.
First of all, your defaults
:
defaults: {
"Product" : null,
"ShortDescription" : null,
"Category" : "food",
"Price" : new PriceModel,
"Date" : new DateModel,
"Uses" : 0,
"Tags" : [],
"Contributor" : null
}
That will end up with one PriceModel
, one DateModel
, and one tags array being shared by all instances of that model. A defaults object is shallow copied and merged into the model's attributes, none of the values in defaults
are cloned or duplicated, they're just copied over as-is. If you want distinced Price
, Date
, and Tags
values then use a function for defaults
:
defaults: function() {
return {
"Product" : null,
"ShortDescription" : null,
"Category" : "food",
"Price" : new PriceModel,
"Date" : new DateModel,
"Uses" : 0,
"Tags" : [],
"Contributor" : null
};
}
The second problem is that set has a fairly simplistic view of what change means. If you have a look at the source for set, you'll see this:
// If the new and previous value differ, record the change. If not,
// then remove changes for this attribute.
if (!_.isEqual(prev[attr], val) || (_.has(now, attr) !== _.has(prev, attr))) {
this.changed[attr] = val;
if (!silent) this._pending[attr] = true;
} else {
delete this.changed[attr];
delete this._pending[attr];
if (!changing) delete this._changes[attr];
}
The _.isEqual won't recognize that something has changed inside your Price
or Date
or that you've added or removed something from Tags
. If you do things like this:
p = new PriceModel(...);
m.set('Price', p)
then m
will noticed that Price
has changed but if you:
p = m.get('Price');
p.set(...);
m.set('Price', p);
then m
won't recognize that Price
has changed; your model won't automatically bind to events on Price
so it won't notice the p.set(...)
call and it won't recognize m.set('Price', p)
as a change since that's little more than a fancy way of saying p = p
.
You can solve part of this change problem by not giving set
a Tags
array that came from get
; make a copy, change the copy, and then hand the updated copy to set
. The half can be handled by binding to "change"
events on the contained Price
and Date
models and forwarding them similar to how collections do it, something like this:
initialize: function() {
this.attributes.Price.on(
'all',
function(ev, model, opts) { this.trigger(ev, model, opts) },
this
);
//...
}
You'd want to provide your own set
implementation in case someone did a set('Price', some_new_object)
and you need to rebind your forwarder.
来源:https://stackoverflow.com/questions/13366248/how-are-change-events-handled-in-backbone-models