Skip to content

Commit 0a4f306

Browse files
committed
fix: shallowReadonly should keep reactive properties reactive
ref #552
1 parent 2dcdbca commit 0a4f306

File tree

2 files changed

+70
-56
lines changed

2 files changed

+70
-56
lines changed

packages/reactivity/__tests__/readonly.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,5 +470,13 @@ describe('reactivity/readonly', () => {
470470
`Set operation on key "foo" failed: target is readonly.`
471471
).not.toHaveBeenWarned()
472472
})
473+
474+
test('should keep reactive properties reactive', () => {
475+
const props: any = shallowReadonly({ n: reactive({ foo: 1 }) })
476+
unlock()
477+
props.n = reactive({ foo: 2 })
478+
lock()
479+
expect(isReactive(props.n)).toBe(true)
480+
})
473481
})
474482
})

packages/reactivity/src/baseHandlers.ts

Lines changed: 62 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ const builtInSymbols = new Set(
1111
.filter(isSymbol)
1212
)
1313

14-
function createGetter(isReadonly: boolean, shallow = false) {
14+
const get = createGetter()
15+
const readonlyGet = createGetter(true)
16+
const shallowReadonlyGet = createGetter(true, true)
17+
18+
function createGetter(isReadonly = false, shallow = false) {
1519
return function get(target: object, key: string | symbol, receiver: object) {
1620
const res = Reflect.get(target, key, receiver)
1721
if (isSymbol(key) && builtInSymbols.has(key)) {
@@ -36,39 +40,60 @@ function createGetter(isReadonly: boolean, shallow = false) {
3640
}
3741
}
3842

39-
function set(
40-
target: object,
41-
key: string | symbol,
42-
value: unknown,
43-
receiver: object
44-
): boolean {
45-
value = toRaw(value)
46-
const oldValue = (target as any)[key]
47-
if (isRef(oldValue) && !isRef(value)) {
48-
oldValue.value = value
49-
return true
50-
}
51-
const hadKey = hasOwn(target, key)
52-
const result = Reflect.set(target, key, value, receiver)
53-
// don't trigger if target is something up in the prototype chain of original
54-
if (target === toRaw(receiver)) {
55-
/* istanbul ignore else */
56-
if (__DEV__) {
57-
const extraInfo = { oldValue, newValue: value }
58-
if (!hadKey) {
59-
trigger(target, TriggerOpTypes.ADD, key, extraInfo)
60-
} else if (hasChanged(value, oldValue)) {
61-
trigger(target, TriggerOpTypes.SET, key, extraInfo)
43+
const set = createSetter()
44+
const readonlySet = createSetter(true)
45+
const shallowReadonlySet = createSetter(true, true)
46+
47+
function createSetter(isReadonly = false, shallow = false) {
48+
return function set(
49+
target: object,
50+
key: string | symbol,
51+
value: unknown,
52+
receiver: object
53+
): boolean {
54+
if (isReadonly && LOCKED) {
55+
if (__DEV__) {
56+
console.warn(
57+
`Set operation on key "${String(key)}" failed: target is readonly.`,
58+
target
59+
)
60+
}
61+
return true
62+
}
63+
64+
const oldValue = (target as any)[key]
65+
if (!shallow) {
66+
value = toRaw(value)
67+
if (isRef(oldValue) && !isRef(value)) {
68+
oldValue.value = value
69+
return true
6270
}
6371
} else {
64-
if (!hadKey) {
65-
trigger(target, TriggerOpTypes.ADD, key)
66-
} else if (hasChanged(value, oldValue)) {
67-
trigger(target, TriggerOpTypes.SET, key)
72+
// in shallow mode, objects are set as-is regardless of reactive or not
73+
}
74+
75+
const hadKey = hasOwn(target, key)
76+
const result = Reflect.set(target, key, value, receiver)
77+
// don't trigger if target is something up in the prototype chain of original
78+
if (target === toRaw(receiver)) {
79+
/* istanbul ignore else */
80+
if (__DEV__) {
81+
const extraInfo = { oldValue, newValue: value }
82+
if (!hadKey) {
83+
trigger(target, TriggerOpTypes.ADD, key, extraInfo)
84+
} else if (hasChanged(value, oldValue)) {
85+
trigger(target, TriggerOpTypes.SET, key, extraInfo)
86+
}
87+
} else {
88+
if (!hadKey) {
89+
trigger(target, TriggerOpTypes.ADD, key)
90+
} else if (hasChanged(value, oldValue)) {
91+
trigger(target, TriggerOpTypes.SET, key)
92+
}
6893
}
6994
}
95+
return result
7096
}
71-
return result
7297
}
7398

7499
function deleteProperty(target: object, key: string | symbol): boolean {
@@ -98,35 +123,18 @@ function ownKeys(target: object): (string | number | symbol)[] {
98123
}
99124

100125
export const mutableHandlers: ProxyHandler<object> = {
101-
get: createGetter(false),
126+
get,
102127
set,
103128
deleteProperty,
104129
has,
105130
ownKeys
106131
}
107132

108133
export const readonlyHandlers: ProxyHandler<object> = {
109-
get: createGetter(true),
110-
111-
set(
112-
target: object,
113-
key: string | symbol,
114-
value: unknown,
115-
receiver: object
116-
): boolean {
117-
if (LOCKED) {
118-
if (__DEV__) {
119-
console.warn(
120-
`Set operation on key "${String(key)}" failed: target is readonly.`,
121-
target
122-
)
123-
}
124-
return true
125-
} else {
126-
return set(target, key, value, receiver)
127-
}
128-
},
129-
134+
get: readonlyGet,
135+
set: readonlySet,
136+
has,
137+
ownKeys,
130138
deleteProperty(target: object, key: string | symbol): boolean {
131139
if (LOCKED) {
132140
if (__DEV__) {
@@ -141,16 +149,14 @@ export const readonlyHandlers: ProxyHandler<object> = {
141149
} else {
142150
return deleteProperty(target, key)
143151
}
144-
},
145-
146-
has,
147-
ownKeys
152+
}
148153
}
149154

150155
// props handlers are special in the sense that it should not unwrap top-level
151156
// refs (in order to allow refs to be explicitly passed down), but should
152157
// retain the reactivity of the normal readonly object.
153158
export const shallowReadonlyHandlers: ProxyHandler<object> = {
154159
...readonlyHandlers,
155-
get: createGetter(true, true)
160+
get: shallowReadonlyGet,
161+
set: shallowReadonlySet
156162
}

0 commit comments

Comments
 (0)