2.5.6
https://jsfiddle.net/DeepOne/62dy96b0/
1) Make an instance of an object with an implicitely 'undefined' property in prototype:
const someObj = function() {};
someObj.prototype.someProp = undefined;
const myObj = new someObj();
Note that now:
2) Use such object instace as data/prop value in Vue, e.g.
3) Try to assign someProp a value and make it reactive:
this.$set(this.value, 'someProp', 'someValue');
Result:
property value is set
Property is expected to become reactive.
Property is not reactive.
The check in src/core/observer/index.js, in function 'set' looks like this:
if (key in target && ...)
It is truthy for properties declared as 'undefined' in object prototype, even if these are not set explicitely on the object!
Therefore the defineReactive call can never be reached.
Described object structure (with prototype.prop = undefined) is not just arbitrary: Swagger.io JS generator actually creates such object models in order to represent missing but possible properties. As we cannot use missing object properties for data binding in Vue, such objects have to be extended, setting missing properties to some value explicitely (e. g. undefined oder empty string etc.)
Even though it's generated by Swagger, prop1 and prop2 should be declared as instance properties if you really want to use a Constructor:
function Obj () {
this.prop1 = undefined
this.prop2 = undefined
}
Otherwise, Vue won't pick them up.
Still, it doesn't look right not to pick it up with $set, I'll investigate a bit further
On a side note, it's not about criticising Swagger.io practice here, but declaring instance properties on the prototype is super weird. You usually put data that you want to share across all instances there (like functions). But here, as soon as you change the value of a local instance, it becomes a new property on the instance 😞
FYI, you can fix it by doing
delete this.value.__proto__.prop2
this.$set(this.value, 'prop2', undefined);
this.value.__proto__.prop2 = undefined
We could check for hasOwnProperty to make this work. Here's a perf comparison https://jsbench.me/zkja8a4rfa/1. It's obviously slower because it's an extra check. @yyx990803 should we support this for consistency or do you think it's too much?
I agree that (mis-)using prototype for this is rather weird. And it's the main reason why I have to re-declare those props in first place. That is, getting a potentially incomplete (or even empty) instance as prop and immediately expanding all missing properties via $set.
delete from proto / $set / restore proto
Hm, this combo would actually work, unless something happens async and another model instance gets initialized while proto is temporarily incomplete. Otherwise I could live with low perf.
I also had hasOwnProperty in mind, though it's definitely up to you, considering how rare this constellation seems to be.
This is intended behavior in order to make another use case possible: class-based model objects with getter/setters defined on the prototype.
Note that we explicitly recommend against using class/prototype based objects as data. The general rule of thumb is Vue's reactivity system leaves everything on the prototype chain alone because we assume the user wants the original behavior on the prototype chain.
My suggestion is instead of trying to expand the missing properties post-observe (inside a watcher), do it pre-observe (before you feed the object into Vue instances) - i.e. setting all of these properties onto the object as own properties before letting Vue making it reactive.
Thanks for the explanation, it makes sense.
Pre-observe would be perfect, as reactivity does work fine for such properties being set e. g. in default prop constructor (instance init seems to be different from $set in that aspect). Too bad there is no suitable location for prop expansion up the chain: The models are nested - that is, any property of a model can be an instance of another model.
Therefore the ideal solution would be to pass each model-based property to an appropriate sub-component, which would auto-expand the corresponding model. It would allow very transparent hierarchical binding via v-model. On contrary, evaluating/expanding child model in parent would completely defeat the purpose of splitting models into separate components.
Anyway, I'm now implementing an own codegen that would provide ready-to-use instances, as the weird prototyping is very likely to cause more problems in the future.
Most helpful comment
This is intended behavior in order to make another use case possible: class-based model objects with getter/setters defined on the prototype.
Note that we explicitly recommend against using class/prototype based objects as data. The general rule of thumb is Vue's reactivity system leaves everything on the prototype chain alone because we assume the user wants the original behavior on the prototype chain.
My suggestion is instead of trying to expand the missing properties post-observe (inside a watcher), do it pre-observe (before you feed the object into Vue instances) - i.e. setting all of these properties onto the object as own properties before letting Vue making it reactive.