Skip to content

Commit a95554d

Browse files
committed
fix(reactivity): fix shallow/readonly edge cases
1 parent 8dcb6c7 commit a95554d

File tree

5 files changed

+69
-14
lines changed

5 files changed

+69
-14
lines changed

packages/reactivity/__tests__/readonly.spec.ts

+13-7
Original file line numberDiff line numberDiff line change
@@ -480,21 +480,27 @@ describe('reactivity/readonly', () => {
480480
const r = ref(false)
481481
const ror = readonly(r)
482482
const obj = reactive({ ror })
483-
try {
483+
expect(() => {
484484
obj.ror = true
485-
} catch (e) {}
486-
485+
}).toThrow()
487486
expect(obj.ror).toBe(false)
488487
})
488+
489489
test('replacing a readonly ref nested in a reactive object with a new ref', () => {
490490
const r = ref(false)
491491
const ror = readonly(r)
492492
const obj = reactive({ ror })
493-
try {
494-
obj.ror = ref(true) as unknown as boolean
495-
} catch (e) {}
496-
493+
obj.ror = ref(true) as unknown as boolean
497494
expect(obj.ror).toBe(true)
498495
expect(toRaw(obj).ror).not.toBe(ror) // ref successfully replaced
499496
})
497+
498+
test('setting readonly object to writable nested ref', () => {
499+
const r = ref<any>()
500+
const obj = reactive({ r })
501+
const ro = readonly({})
502+
obj.r = ro
503+
expect(obj.r).toBe(ro)
504+
expect(r.value).toBe(ro)
505+
})
500506
})

packages/reactivity/__tests__/ref.spec.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from '../src/index'
1111
import { computed } from '@vue/runtime-dom'
1212
import { shallowRef, unref, customRef, triggerRef } from '../src/ref'
13-
import { isShallow } from '../src/reactive'
13+
import { isShallow, readonly, shallowReactive } from '../src/reactive'
1414

1515
describe('reactivity/ref', () => {
1616
it('should hold a value', () => {
@@ -400,4 +400,22 @@ describe('reactivity/ref', () => {
400400
b.value = obj
401401
expect(spy2).toBeCalledTimes(1)
402402
})
403+
404+
test('ref should preserve value shallow/readonly-ness', () => {
405+
const original = {}
406+
const r = reactive(original)
407+
const s = shallowReactive(original)
408+
const rr = readonly(original)
409+
const a = ref(original)
410+
411+
expect(a.value).toBe(r)
412+
413+
a.value = s
414+
expect(a.value).toBe(s)
415+
expect(a.value).not.toBe(r)
416+
417+
a.value = rr
418+
expect(a.value).toBe(rr)
419+
expect(a.value).not.toBe(r)
420+
})
403421
})

packages/reactivity/__tests__/shallowReactive.spec.ts

+22
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from '../src/reactive'
88

99
import { effect } from '../src/effect'
10+
import { Ref, isRef, ref } from '../src/ref'
1011

1112
describe('shallowReactive', () => {
1213
test('should not make non-reactive properties reactive', () => {
@@ -46,6 +47,27 @@ describe('shallowReactive', () => {
4647
expect(isReactive(r.foo.bar)).toBe(false)
4748
})
4849

50+
// vuejs/vue#12597
51+
test('should not unwrap refs', () => {
52+
const foo = shallowReactive({
53+
bar: ref(123)
54+
})
55+
expect(isRef(foo.bar)).toBe(true)
56+
expect(foo.bar.value).toBe(123)
57+
})
58+
59+
// vuejs/vue#12688
60+
test('should not mutate refs', () => {
61+
const original = ref(123)
62+
const foo = shallowReactive<{ bar: Ref<number> | number }>({
63+
bar: original
64+
})
65+
expect(foo.bar).toBe(original)
66+
foo.bar = 234
67+
expect(foo.bar).toBe(234)
68+
expect(original.value).toBe(123)
69+
})
70+
4971
test('should respect shallow/deep versions of same target on access', () => {
5072
const original = {}
5173
const shallow = shallowReactive(original)

packages/reactivity/src/baseHandlers.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,10 @@ function createSetter(shallow = false) {
158158
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
159159
return false
160160
}
161-
if (!shallow && !isReadonly(value)) {
162-
if (!isShallow(value)) {
163-
value = toRaw(value)
161+
if (!shallow) {
162+
if (!isShallow(value) && !isReadonly(value)) {
164163
oldValue = toRaw(oldValue)
164+
value = toRaw(value)
165165
}
166166
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
167167
oldValue.value = value

packages/reactivity/src/ref.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ import {
66
} from './effect'
77
import { TrackOpTypes, TriggerOpTypes } from './operations'
88
import { isArray, hasChanged, IfAny } from '@vue/shared'
9-
import { isProxy, toRaw, isReactive, toReactive } from './reactive'
9+
import {
10+
isProxy,
11+
toRaw,
12+
isReactive,
13+
toReactive,
14+
isReadonly,
15+
isShallow
16+
} from './reactive'
1017
import type { ShallowReactiveMarker } from './reactive'
1118
import { CollectionTypes } from './collectionHandlers'
1219
import { createDep, Dep } from './dep'
@@ -112,10 +119,12 @@ class RefImpl<T> {
112119
}
113120

114121
set value(newVal) {
115-
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
122+
const useDirectValue =
123+
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
124+
newVal = useDirectValue ? newVal : toRaw(newVal)
116125
if (hasChanged(newVal, this._rawValue)) {
117126
this._rawValue = newVal
118-
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
127+
this._value = useDirectValue ? newVal : toReactive(newVal)
119128
triggerRefValue(this, newVal)
120129
}
121130
}

0 commit comments

Comments
 (0)