diff --git a/packages/reactivity/__tests__/reactive.spec.ts b/packages/reactivity/__tests__/reactive.spec.ts index f6fed0230c3..6592ce68825 100644 --- a/packages/reactivity/__tests__/reactive.spec.ts +++ b/packages/reactivity/__tests__/reactive.spec.ts @@ -272,4 +272,83 @@ describe('reactivity/reactive', () => { const observed = reactive(original) expect(isReactive(observed)).toBe(false) }) + + describe('with data property', () => { + test('should return the original value if a property is non-configurable, non-writable', () => { + const obj: any = {} + const inner = { foo: 1 } + Object.defineProperty(obj, 'foo', { + value: inner, + writable: false, + configurable: false + }) + + const observed = reactive(obj) + expect(isReactive(observed.foo)).toBe(false) + expect(observed.foo).toBe(inner) + }) + + test('should return proxy value if a property is configurable or writable', () => { + const obj: any = {} + Object.defineProperty(obj, 'foo', { + value: { foo: 1 }, + writable: true, + configurable: false + }) + Object.defineProperty(obj, 'bar', { + value: { foo: 1 }, + writable: false, + configurable: true + }) + Object.defineProperty(obj, 'baz', { + value: { foo: 1 }, + writable: true, + configurable: true + }) + + const observed = reactive(obj) + expect(isReactive(observed.foo)).toBe(true) + expect(isReactive(observed.bar)).toBe(true) + expect(isReactive(observed.baz)).toBe(true) + }) + + // https://www.ecma-international.org/ecma-262/11.0/index.html#sec-invariants-of-the-essential-internal-methods + // https://www.ecma-international.org/ecma-262/11.0/index.html#sec-samevalue + test('should be able to set the value if they are the SameValue', () => { + const inner = { foo: 1 } + const outer: any = {} + Object.defineProperty(outer, 'key', { + value: inner, + configurable: false, + writable: false + }) + + const observed = reactive(outer) + + observed.key = inner + + expect(() => { + observed.key = 'other value' + }).toThrow(`'set' on proxy: trap returned falsish for property 'key'`) + }) + }) + + describe('with accessor property', () => { + test('should return proxy value if a property is an accessor property', () => { + const inner = { foo: 1 } + const outer: any = {} + Object.defineProperty(outer, 'key', { + get() { + return inner + } + }) + + const observed = reactive(outer) + expect(observed).not.toBe(outer) + expect(isReactive(observed)).toBe(true) + expect(isReactive(outer)).toBe(false) + + expect(isReactive(observed.key)).toBe(true) + }) + }) }) diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index e4f9f2a3ff7..27be58d9a50 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -126,6 +126,19 @@ function createGetter(isReadonly = false, shallow = false) { } if (isObject(res)) { + // #3024 + // https://www.ecma-international.org/ecma-262/11.0/index.html#sec-invariants-of-the-essential-internal-methods + const descriptor = Reflect.getOwnPropertyDescriptor(target, key) + if (descriptor) { + if ( + // make sure it's a data property + !(descriptor.get || descriptor.set) && + // non-configurable, non-writable + (!descriptor.configurable && !descriptor.writable) + ) { + return res + } + } // Convert returned value into a proxy as well. we do the isObject check // here to avoid invalid value warning. Also need to lazy access readonly // and reactive here to avoid circular dependency. @@ -147,6 +160,19 @@ function createSetter(shallow = false) { receiver: object ): boolean { let oldValue = (target as any)[key] + + const descriptor = Reflect.getOwnPropertyDescriptor(target, key) + if (descriptor) { + if ( + // make sure it's a data property + !(descriptor.get || descriptor.set) && + // non-configurable, non-writable + (!descriptor.configurable && !descriptor.writable) + ) { + return Object.is(oldValue, value) + } + } + if (!shallow) { value = toRaw(value) oldValue = toRaw(oldValue)