diff --git a/packages/reactivity/__tests__/reactive.spec.ts b/packages/reactivity/__tests__/reactive.spec.ts index ed7097aeb67..f643c5b29c4 100644 --- a/packages/reactivity/__tests__/reactive.spec.ts +++ b/packages/reactivity/__tests__/reactive.spec.ts @@ -112,6 +112,51 @@ describe('reactivity/reactive', () => { expect(dummy).toBe(false) }) + // #10483 + test('observing object with custom Symbol.toStringTag', () => { + const original = { [Symbol.toStringTag]: 'Custom', foo: 1 } + const observed = reactive(original) + + expect(isReactive(observed)).toBe(true) + + let dummy + effect(() => (dummy = observed.foo)) + expect(dummy).toBe(1) + observed.foo = 2 + expect(dummy).toBe(2) + }) + + // #10483 + test('observing collection subtypes with custom Symbol.toStringTag', () => { + class CustomMap extends Map { + get [Symbol.toStringTag]() { + return 'CustomMap' + } + } + const cmap = reactive(new CustomMap()) + expect(isReactive(cmap)).toBe(true) + + let dummy + effect(() => (dummy = cmap.has('key'))) + expect(dummy).toBe(false) + cmap.set('key', 'value') + expect(dummy).toBe(true) + + class CustomArray extends Array { + get [Symbol.toStringTag]() { + return 'CustomArray' + } + } + const carr = reactive(new CustomArray()) + expect(isReactive(carr)).toBe(true) + + let len + effect(() => (len = carr.length)) + expect(len).toBe(0) + carr.push(1) + expect(len).toBe(1) + }) + // #8647 test('observing Set with reactive initial value', () => { const observed = reactive({}) diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index c9a412eb893..dbdf80e9ba7 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -40,8 +40,8 @@ enum TargetType { COLLECTION = 2, } -function targetTypeMap(rawType: string) { - switch (rawType) { +function targetTypeMap(value: Target) { + switch (toRawType(value)) { case 'Object': case 'Array': return TargetType.COMMON @@ -51,14 +51,32 @@ function targetTypeMap(rawType: string) { case 'WeakSet': return TargetType.COLLECTION default: + // a custom `Symbol.toStringTag` changes the result of `toRawType`, so + // fall back to prototype checks to detect built-in types and subtypes. + if ( + value instanceof Map || + value instanceof Set || + value instanceof WeakMap || + value instanceof WeakSet + ) { + return TargetType.COLLECTION + } + if (value instanceof Array || isPlainObject(value)) { + return TargetType.COMMON + } return TargetType.INVALID } } +function isPlainObject(value: Target) { + const proto = Object.getPrototypeOf(value) + return proto === Object.prototype || proto === null +} + function getTargetType(value: Target) { return value[ReactiveFlags.SKIP] || !Object.isExtensible(value) ? TargetType.INVALID - : targetTypeMap(toRawType(value)) + : targetTypeMap(value) } // only unwrap nested ref