diff --git a/packages/reactivity/__tests__/reactive.spec.ts b/packages/reactivity/__tests__/reactive.spec.ts index aabd954568a..1ff8bac37b5 100644 --- a/packages/reactivity/__tests__/reactive.spec.ts +++ b/packages/reactivity/__tests__/reactive.spec.ts @@ -112,6 +112,80 @@ describe('reactivity/reactive', () => { expect(dummy).toBe(false) }) + test('reactive object with custom Symbol.toStringTag triggers reactivity', () => { + const original = { [Symbol.toStringTag]: 'Goat', foo: 1 } + const observed = reactive(original) + + expect(isReactive(observed)).toBe(true) + expect(isProxy(observed)).toBe(true) + + let dummy: number | undefined + effect(() => { + dummy = observed.foo + }) + + expect(dummy).toBe(1) + + observed.foo = 2 + expect(dummy).toBe(2) + }) + + test('custom collection type with custom Symbol.toStringTag is handled as a collection', () => { + class MyCustomMap extends Map { + get [Symbol.toStringTag]() { + return 'MyCustomMap' + } + } + + const myCustomMap = new MyCustomMap() + + expect(Object.prototype.toString.call(myCustomMap)).toBe( + '[object MyCustomMap]', + ) + + const observed = reactive(myCustomMap) + + expect(isReactive(observed)).toBe(true) + expect(isProxy(observed)).toBe(true) + + let dummy: boolean = false + effect(() => { + dummy = observed.has('foo') + }) + + expect(dummy).toBe(false) + + observed.set('foo', 'bar') + expect(dummy).toBe(true) + }) + + test('custom array type with custom Symbol.toStringTag is handled as a common object', () => { + class MyArray extends Array { + get [Symbol.toStringTag]() { + return 'MyArray' + } + } + + const myArr = new MyArray() + + expect(Object.prototype.toString.call(myArr)).toBe('[object MyArray]') + + const observed = reactive(myArr) + + expect(isReactive(observed)).toBe(true) + expect(isProxy(observed)).toBe(true) + + let dummy: number = 0 + effect(() => { + dummy = observed.length + }) + + expect(dummy).toBe(0) + + observed.push(42) + expect(dummy).toBe(1) + }) + test('observed value should proxy mutations to original (Object)', () => { const original: any = { foo: 1 } const observed = reactive(original) diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index 729c854965e..f57231816d4 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -40,7 +40,8 @@ enum TargetType { COLLECTION = 2, } -function targetTypeMap(rawType: string) { +function targetTypeMap(value: Target): TargetType { + const rawType = toRawType(value) switch (rawType) { case 'Object': case 'Array': @@ -51,14 +52,34 @@ function targetTypeMap(rawType: string) { case 'WeakSet': return TargetType.COLLECTION default: + 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: unknown): value is object { + return ( + typeof value === 'object' && + value !== null && + (Object.getPrototypeOf(value) === Object.prototype || + Object.getPrototypeOf(value) === null) + ) +} + function getTargetType(value: Target) { return value[ReactiveFlags.SKIP] || !Object.isExtensible(value) ? TargetType.INVALID - : targetTypeMap(toRawType(value)) + : targetTypeMap(value) } // only unwrap nested ref