Skip to content

Commit c3aff0d

Browse files
committed
more reliably handle prop reactivity (fix #2569)
1 parent 4a71e01 commit c3aff0d

File tree

5 files changed

+94
-38
lines changed

5 files changed

+94
-38
lines changed

src/compiler/compile-props.js

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import config from '../config'
22
import { parseDirective } from '../parsers/directive'
3-
import { isSimplePath } from '../parsers/expression'
43
import { defineReactive } from '../observer/index'
54
import propDef from '../directives/internal/prop'
65
import {
@@ -226,15 +225,7 @@ export function initProp (vm, prop, value) {
226225
value = getPropDefaultValue(vm, prop.options)
227226
}
228227
if (assertProp(prop, value)) {
229-
var doNotObserve =
230-
// if the passed down prop was already converted, then
231-
// subsequent sets should also be converted, because the user
232-
// may mutate the prop binding in the child component (#2549)
233-
!(value && value.__ob__) &&
234-
// otherwise we can skip observation for props that are either
235-
// literal or points to a simple path (non-derived values)
236-
(!prop.dynamic || isSimplePath(prop.raw))
237-
defineReactive(vm, key, value, doNotObserve)
228+
defineReactive(vm, key, value)
238229
}
239230
}
240231

src/directives/internal/prop.js

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,37 @@
55

66
import Watcher from '../../watcher'
77
import config from '../../config'
8+
import { isSimplePath } from '../../parsers/expression'
9+
import { withoutConversion } from '../../observer/index'
810
import { assertProp, initProp, coerceProp } from '../../compiler/compile-props'
911

1012
const bindingModes = config._propBindingModes
1113

1214
export default {
1315

1416
bind () {
15-
var child = this.vm
16-
var parent = child._context
17+
const child = this.vm
18+
const parent = child._context
1719
// passed in from compiler directly
18-
var prop = this.descriptor.prop
19-
var childKey = prop.path
20-
var parentKey = prop.parentPath
21-
var twoWay = prop.mode === bindingModes.TWO_WAY
20+
const prop = this.descriptor.prop
21+
const childKey = prop.path
22+
const parentKey = prop.parentPath
23+
const twoWay = prop.mode === bindingModes.TWO_WAY
24+
const isSimple = isSimplePath(parentKey)
2225

23-
var parentWatcher = this.parentWatcher = new Watcher(
26+
const parentWatcher = this.parentWatcher = new Watcher(
2427
parent,
2528
parentKey,
2629
function (val) {
2730
val = coerceProp(prop, val)
2831
if (assertProp(prop, val)) {
29-
child[childKey] = val
32+
if (isSimple) {
33+
withoutConversion(() => {
34+
child[childKey] = val
35+
})
36+
} else {
37+
child[childKey] = val
38+
}
3039
}
3140
}, {
3241
twoWay: twoWay,
@@ -38,7 +47,14 @@ export default {
3847
)
3948

4049
// set the child initial value.
41-
initProp(child, prop, parentWatcher.value)
50+
const value = parentWatcher.value
51+
if (isSimple && value !== undefined) {
52+
withoutConversion(() => {
53+
initProp(child, prop, value)
54+
})
55+
} else {
56+
initProp(child, prop, value)
57+
}
4258

4359
// setup two-way binding
4460
if (twoWay) {

src/directives/public/for.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import FragmentFactory from '../../fragment/factory'
22
import { FOR } from '../priorities'
3+
import { withoutConversion } from '../../observer/index'
34
import {
45
isObject,
56
warn,
@@ -143,7 +144,9 @@ const vFor = {
143144
// update data for track-by, object repeat &
144145
// primitive values.
145146
if (trackByKey || convertedFromObject || primitive) {
146-
frag.scope[alias] = value
147+
withoutConversion(() => {
148+
frag.scope[alias] = value
149+
})
147150
}
148151
} else { // new isntance
149152
frag = this.create(value, alias, i, key)
@@ -238,7 +241,11 @@ const vFor = {
238241
// for two-way binding on alias
239242
scope.$forContext = this
240243
// define scope properties
241-
defineReactive(scope, alias, value, true /* do not observe */)
244+
// important: define the scope alias without forced conversion
245+
// so that frozen data structures remain non-reactive.
246+
withoutConversion(() => {
247+
defineReactive(scope, alias, value)
248+
})
242249
defineReactive(scope, '$index', index)
243250
if (key) {
244251
defineReactive(scope, '$key', key)

src/observer/index.js

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import Dep from './dep'
22
import { arrayMethods } from './array'
33
import {
44
def,
5-
isObject,
65
isArray,
76
isPlainObject,
87
hasProto,
@@ -11,6 +10,23 @@ import {
1110

1211
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
1312

13+
/**
14+
* By default, when a reactive property is set, the new value is
15+
* also converted to become reactive. However in certain cases, e.g.
16+
* v-for scope alias and props, we don't want to force conversion
17+
* because the value may be a nested value under a frozen data structure.
18+
*
19+
* So whenever we want to set a reactive property without forcing
20+
* conversion on the new value, we wrap that call inside this function.
21+
*/
22+
23+
let shouldConvert = true
24+
export function withoutConversion (fn) {
25+
shouldConvert = false
26+
fn()
27+
shouldConvert = true
28+
}
29+
1430
/**
1531
* Observer class that are attached to each observed
1632
* object. Once attached, the observer converts target
@@ -154,6 +170,7 @@ export function observe (value, vm) {
154170
) {
155171
ob = value.__ob__
156172
} else if (
173+
shouldConvert &&
157174
(isArray(value) || isPlainObject(value)) &&
158175
Object.isExtensible(value) &&
159176
!value._isVue
@@ -172,10 +189,9 @@ export function observe (value, vm) {
172189
* @param {Object} obj
173190
* @param {String} key
174191
* @param {*} val
175-
* @param {Boolean} doNotObserve
176192
*/
177193

178-
export function defineReactive (obj, key, val, doNotObserve) {
194+
export function defineReactive (obj, key, val) {
179195
var dep = new Dep()
180196

181197
var property = Object.getOwnPropertyDescriptor(obj, key)
@@ -187,13 +203,7 @@ export function defineReactive (obj, key, val, doNotObserve) {
187203
var getter = property && property.get
188204
var setter = property && property.set
189205

190-
// if doNotObserve is true, only use the child value observer
191-
// if it already exists, and do not attempt to create it.
192-
// this allows freezing a large object from the root and
193-
// avoid unnecessary observation inside v-for fragments.
194-
var childOb = doNotObserve
195-
? isObject(val) && val.__ob__
196-
: observe(val)
206+
var childOb = observe(val)
197207
Object.defineProperty(obj, key, {
198208
enumerable: true,
199209
configurable: true,
@@ -223,9 +233,7 @@ export function defineReactive (obj, key, val, doNotObserve) {
223233
} else {
224234
val = newVal
225235
}
226-
childOb = doNotObserve
227-
? isObject(newVal) && newVal.__ob__
228-
: observe(newVal)
236+
childOb = observe(newVal)
229237
dep.notify()
230238
}
231239
})

test/unit/specs/directives/internal/prop_spec.js

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -678,11 +678,13 @@ describe('prop', function () {
678678

679679
it('non reactive values passed down as prop should not be converted', function (done) {
680680
var a = Object.freeze({
681-
msg: 'hello'
681+
nested: {
682+
msg: 'hello'
683+
}
682684
})
683685
var parent = new Vue({
684686
el: el,
685-
template: '<comp :a="a"></comp>',
687+
template: '<comp :a="a.nested"></comp>',
686688
data: {
687689
a: a
688690
},
@@ -695,7 +697,11 @@ describe('prop', function () {
695697
var child = parent.$children[0]
696698
expect(child.a.msg).toBe('hello')
697699
expect(child.a.__ob__).toBeUndefined() // should not be converted
698-
parent.a = Object.freeze({ msg: 'yo' })
700+
parent.a = Object.freeze({
701+
nested: {
702+
msg: 'yo'
703+
}
704+
})
699705
Vue.nextTick(function () {
700706
expect(child.a.msg).toBe('yo')
701707
expect(child.a.__ob__).toBeUndefined()
@@ -723,7 +729,7 @@ describe('prop', function () {
723729
})
724730

725731
// #2549
726-
it('mutating child prop binding should be reactive if parent value was reactive', function (done) {
732+
it('mutating child prop binding should be reactive', function (done) {
727733
var vm = new Vue({
728734
el: el,
729735
template: '<comp :list="list"></comp>',
@@ -747,4 +753,32 @@ describe('prop', function () {
747753
done()
748754
})
749755
})
756+
757+
it('prop default value should be reactive', function (done) {
758+
var vm = new Vue({
759+
el: el,
760+
template: '<comp :list="list"></comp>',
761+
data: {
762+
list: undefined
763+
},
764+
components: {
765+
comp: {
766+
props: {
767+
list: {
768+
default: function () {
769+
return [2, 3, 4]
770+
}
771+
}
772+
},
773+
template: '<div v-for="i in list">{{ i }}</div>'
774+
}
775+
}
776+
})
777+
expect(vm.$el.textContent).toBe('234')
778+
vm.$children[0].list.push(5)
779+
Vue.nextTick(function () {
780+
expect(vm.$el.textContent).toBe('2345')
781+
done()
782+
})
783+
})
750784
})

0 commit comments

Comments
 (0)