问题
Getting some weird behaviour I don't quite get when using the KO mapping plugin with subclassed objects.
Use case:
The server has a number of simple model classes used for the AJAX API.
I want to generate a stub class that generates KO observables for properties on these classes, so that if I change the server code the stub classes are automatically updated with an observable for the new property - I need this so I can create brand new objects without mapping from existing data.
I then want to be able to create a subclass of the stub that has e.g. computeds for the UI, action methods, client state etc. These additional properties do not need to be mapped back to the server - in fact they cannot be, as the server models will have them (or they'd be in the stub)
I am having problems getting this working.
Here is some cut down code:
function ItemModelBase(data, mapping)
{
if(data)
ko.mapping.fromJS(data, mapping, this);
this.Name = this.Name || ko.observable();
}
function ItemModel(data, mapping)
{
var _this = this;
ItemModelBase.call(this, data, mapping);
this.Salutation = ko.computed(function() { return 'Hello ' + _this.Name(); });
}
// Comment this out and it works
ItemModel.prototype = new ItemModelBase;
function Model()
{
var _this = this;
var _mapping = {
'Items': {
create: function(o) { return new ItemModel(o.data); }
}
};
this.Items = ko.observableArray();
this.load = function()
{
ko.mapping.fromJS({
Items: [
{ Name: 'Aardvark' },
{ Name: 'Bat' },
{ Name: 'Cheetah' },
{ Name: 'Duiker' },
{ Name: 'Ocelot' }
]
}, _mapping, _this);
};
}
var model = new Model();
ko.applyBindings(model);
model.load();
Using view:
<ul data-bind="foreach: Items">
<li>
<span data-bind="text: Name"></span>:
<span data-bind="text: Salutation"></span>
</li>
</ul>
What this displays is a list of the correct number of items, but only using data from the last item - result:
- Ocelot: Hello Ocelot
- Ocelot: Hello Ocelot
- Ocelot: Hello Ocelot
- Ocelot: Hello Ocelot
- Ocelot: Hello Ocelot
If I don't subclass the base object, i.e. I comment out the ItemModel.prototype = new ItemModelBase; line, it works as expected.
My main language is C# and I find Javascript's inheritance very difficult to get my head round; what is going on here? As far as I can tell, without the prototype it works because the base constructor call is just A.N. Other JS call that is adding a load of properties to the current instance.
This works for this case, but presumably means if I add some common functions to the base class prototype I won't be able to use them?
I guess I need a main single question, so it is: Why is only the last item being displayed, and what do I do about it?
JFfiddle: https://jsfiddle.net/whelkaholism/d427mssa/
EDIT: If I include the inheritance and then create a new model, that new model has the properties of the last item too. REALLY don't understand that, but it looks like it's assigning values to the prototype not the instance?
console.log(ko.toJSON(new ItemModel()));
Results in:
{"Name":"Ocelot","Salutation":"Hello Ocelot"}
EDIT: Tomalak posted an answer which seems to have been deleted, which is a shame as although I still don't know what is going on, it did actually give a fix.
The problem is this: this.Name = this.Name || ko.observable();
If I move the observable definition above the mapping it works:
function ItemModelBase(data, mapping)
{
this.Name = ko.observable();
// Exhibits the same bug as original code
// this.Name = this.Name || ko.observable();
if(data)
ko.mapping.fromJS(data, mapping, this);
}
Oddly leaving the condition it results in the same bug, even though my understanding of JS was that this.Name would be undefined at that point, resulting that statement having the value of `ko.observable()' and being identical to the code without the cnodition?
回答1:
Don't attempt to create computeds or observables anywhere up the prototype chain. Here's what you can and can't do:
You can modify the prototype to share share common, non-observable (!) properties and methods between viewmodels.
var common = { baz: function () { return ko.unwrap(this.bar) || ko.unwrap(this.foo); } }; function VM1() { this.foo = ko.observable(); } VM1.prototype = common; function VM2() { this.foo = ko.observable(); this.bar = ko.observable(); } VM2.prototype = common; var vm2 = new VM2(); vm2.foo("FOO"); vm2.baz(); // -> returns "FOO";You can't (read: shouldn't) have other viewmodels in the prototype chain. Since all prototypes are living object instances themselves, all their observables would be shared between your actual viewmodel instances. The only situation where doing this would make sense is if you wanted to share subscribable (!) data between instances.
function VM0() { this.count = ko.observable(0); } VM0.prototype.increment = function () { this.count(this.count() + 1); }; function VM1() { this.foo = ko.observable(); this.count.subscribe(function (newCount) { // count has increased across all VM1 instances }); } VM1.prototype = new VM0(); var vm1 = new VM1(); vm1.inc(); // -> triggers subscription across all VM1 instancesYou can use functions to attach common, self-contained functionality (including observables) to a viewmodel instance. That's basically what applying a different constructor like
ItemModelBase.call(this, ...);would do.function VM1() { this.foo = ko.observable(); } function VM2() { VM1.call(this); // creates foo observable this.bar = ko.observable(); }You cannot call
ko.mappingmore than once on the same viewmodel instance (i.e. first map basic properties, then specific ones). The mapping plugin keeps its internal state in an instance variable itself - a second run would overwrite the state of the first. (Of course you can still map the same list of properties a second time, i.e. update their values.)
All of this leaves you with pretty little room for a C# type inheritance hierarchy. Unless you have common functionality to share, don't bother building a viewmodel hierarchy in JS.
回答2:
I think the issue is your test for whether a Name property exists. Because it exists in the prototype, it exists. If you use
if (!this.hasOwnProperty('Name')) { this.Name = ko.observable(); }
for your test-and-assign, it should do what you expect.
来源:https://stackoverflow.com/questions/30504149/problems-with-knockout-mapping-when-using-object-inheritance