fix: shallowReadonly should keep reactive properties reactive

ref #552
This commit is contained in:
Evan You 2019-12-20 11:14:07 -05:00
parent 2dcdbca6e2
commit 0a4f306492
2 changed files with 70 additions and 56 deletions

View File

@ -470,5 +470,13 @@ describe('reactivity/readonly', () => {
`Set operation on key "foo" failed: target is readonly.` `Set operation on key "foo" failed: target is readonly.`
).not.toHaveBeenWarned() ).not.toHaveBeenWarned()
}) })
test('should keep reactive properties reactive', () => {
const props: any = shallowReadonly({ n: reactive({ foo: 1 }) })
unlock()
props.n = reactive({ foo: 2 })
lock()
expect(isReactive(props.n)).toBe(true)
})
}) })
}) })

View File

@ -11,7 +11,11 @@ const builtInSymbols = new Set(
.filter(isSymbol) .filter(isSymbol)
) )
function createGetter(isReadonly: boolean, shallow = false) { const get = createGetter()
const readonlyGet = createGetter(true)
const shallowReadonlyGet = createGetter(true, true)
function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) { return function get(target: object, key: string | symbol, receiver: object) {
const res = Reflect.get(target, key, receiver) const res = Reflect.get(target, key, receiver)
if (isSymbol(key) && builtInSymbols.has(key)) { if (isSymbol(key) && builtInSymbols.has(key)) {
@ -36,39 +40,60 @@ function createGetter(isReadonly: boolean, shallow = false) {
} }
} }
function set( const set = createSetter()
target: object, const readonlySet = createSetter(true)
key: string | symbol, const shallowReadonlySet = createSetter(true, true)
value: unknown,
receiver: object function createSetter(isReadonly = false, shallow = false) {
): boolean { return function set(
value = toRaw(value) target: object,
const oldValue = (target as any)[key] key: string | symbol,
if (isRef(oldValue) && !isRef(value)) { value: unknown,
oldValue.value = value receiver: object
return true ): boolean {
} if (isReadonly && LOCKED) {
const hadKey = hasOwn(target, key) if (__DEV__) {
const result = Reflect.set(target, key, value, receiver) console.warn(
// don't trigger if target is something up in the prototype chain of original `Set operation on key "${String(key)}" failed: target is readonly.`,
if (target === toRaw(receiver)) { target
/* istanbul ignore else */ )
if (__DEV__) { }
const extraInfo = { oldValue, newValue: value } return true
if (!hadKey) { }
trigger(target, TriggerOpTypes.ADD, key, extraInfo)
} else if (hasChanged(value, oldValue)) { const oldValue = (target as any)[key]
trigger(target, TriggerOpTypes.SET, key, extraInfo) if (!shallow) {
value = toRaw(value)
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
} }
} else { } else {
if (!hadKey) { // in shallow mode, objects are set as-is regardless of reactive or not
trigger(target, TriggerOpTypes.ADD, key) }
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key) const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
/* istanbul ignore else */
if (__DEV__) {
const extraInfo = { oldValue, newValue: value }
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, extraInfo)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, extraInfo)
}
} else {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key)
}
} }
} }
return result
} }
return result
} }
function deleteProperty(target: object, key: string | symbol): boolean { function deleteProperty(target: object, key: string | symbol): boolean {
@ -98,7 +123,7 @@ function ownKeys(target: object): (string | number | symbol)[] {
} }
export const mutableHandlers: ProxyHandler<object> = { export const mutableHandlers: ProxyHandler<object> = {
get: createGetter(false), get,
set, set,
deleteProperty, deleteProperty,
has, has,
@ -106,27 +131,10 @@ export const mutableHandlers: ProxyHandler<object> = {
} }
export const readonlyHandlers: ProxyHandler<object> = { export const readonlyHandlers: ProxyHandler<object> = {
get: createGetter(true), get: readonlyGet,
set: readonlySet,
set( has,
target: object, ownKeys,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
if (LOCKED) {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
} else {
return set(target, key, value, receiver)
}
},
deleteProperty(target: object, key: string | symbol): boolean { deleteProperty(target: object, key: string | symbol): boolean {
if (LOCKED) { if (LOCKED) {
if (__DEV__) { if (__DEV__) {
@ -141,10 +149,7 @@ export const readonlyHandlers: ProxyHandler<object> = {
} else { } else {
return deleteProperty(target, key) return deleteProperty(target, key)
} }
}, }
has,
ownKeys
} }
// props handlers are special in the sense that it should not unwrap top-level // props handlers are special in the sense that it should not unwrap top-level
@ -152,5 +157,6 @@ export const readonlyHandlers: ProxyHandler<object> = {
// retain the reactivity of the normal readonly object. // retain the reactivity of the normal readonly object.
export const shallowReadonlyHandlers: ProxyHandler<object> = { export const shallowReadonlyHandlers: ProxyHandler<object> = {
...readonlyHandlers, ...readonlyHandlers,
get: createGetter(true, true) get: shallowReadonlyGet,
set: shallowReadonlySet
} }