diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index 24ae1515610..6ff76c671b3 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -6,7 +6,8 @@ import { capitalize, hasOwn, hasChanged, - toRawType + toRawType, + isMap } from '@vue/shared' export type CollectionTypes = IterableCollections | WeakCollections @@ -129,7 +130,7 @@ function clear(this: IterableCollections) { const target = toRaw(this) const hadItems = target.size !== 0 const oldTarget = __DEV__ - ? target instanceof Map + ? isMap(target) ? new Map(target) : new Set(target) : undefined @@ -185,9 +186,10 @@ function createIterableMethod( ): Iterable & Iterator { const target = (this as any)[ReactiveFlags.RAW] const rawTarget = toRaw(target) - const isMap = rawTarget instanceof Map - const isPair = method === 'entries' || (method === Symbol.iterator && isMap) - const isKeyOnly = method === 'keys' && isMap + const targetIsMap = isMap(rawTarget) + const isPair = + method === 'entries' || (method === Symbol.iterator && targetIsMap) + const isKeyOnly = method === 'keys' && targetIsMap const innerIterator = target[method](...args) const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive !isReadonly && diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index a4d087beade..495d6714939 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -1,5 +1,5 @@ import { TrackOpTypes, TriggerOpTypes } from './operations' -import { EMPTY_OBJ, isArray, isIntegerKey } from '@vue/shared' +import { EMPTY_OBJ, isArray, isIntegerKey, isMap } from '@vue/shared' // The main WeakMap that stores {target -> key -> dep} connections. // Conceptually, it's easier to think of a dependency as a Dep class @@ -197,19 +197,33 @@ export function trigger( if (key !== void 0) { add(depsMap.get(key)) } + // also run for iteration key on ADD | DELETE | Map.SET - const shouldTriggerIteration = - (type === TriggerOpTypes.ADD && - (!isArray(target) || isIntegerKey(key))) || - (type === TriggerOpTypes.DELETE && !isArray(target)) - if ( - shouldTriggerIteration || - (type === TriggerOpTypes.SET && target instanceof Map) - ) { - add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY)) - } - if (shouldTriggerIteration && target instanceof Map) { - add(depsMap.get(MAP_KEY_ITERATE_KEY)) + switch (type) { + case TriggerOpTypes.ADD: + if (!isArray(target)) { + add(depsMap.get(ITERATE_KEY)) + if (isMap(target)) { + add(depsMap.get(MAP_KEY_ITERATE_KEY)) + } + } else if (isIntegerKey(key)) { + // new index added to array -> length changes + add(depsMap.get('length')) + } + break + case TriggerOpTypes.DELETE: + if (!isArray(target)) { + add(depsMap.get(ITERATE_KEY)) + if (isMap(target)) { + add(depsMap.get(MAP_KEY_ITERATE_KEY)) + } + } + break + case TriggerOpTypes.SET: + if (isMap(target)) { + add(depsMap.get(ITERATE_KEY)) + } + break } } diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 9c131067feb..ac1b1d16d7f 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -16,7 +16,9 @@ import { isString, hasChanged, NOOP, - remove + remove, + isMap, + isSet } from '@vue/shared' import { currentInstance, @@ -335,12 +337,12 @@ function traverse(value: unknown, seen: Set = new Set()) { for (let i = 0; i < value.length; i++) { traverse(value[i], seen) } - } else if (value instanceof Map) { - value.forEach((v, key) => { + } else if (isMap(value)) { + value.forEach((_, key) => { // to register mutation dep for existing keys traverse(value.get(key), seen) }) - } else if (value instanceof Set) { + } else if (isSet(value)) { value.forEach(v => { traverse(v, seen) }) diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 2178c5f1ee7..7daeaa5242c 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -58,9 +58,11 @@ export const hasOwn = ( ): key is keyof typeof val => hasOwnProperty.call(val, key) export const isArray = Array.isArray -export const isSet = (val: any): boolean => { - return toRawType(val) === 'Set' -} +export const isMap = (val: unknown): val is Map => + toTypeString(val) === '[object Map]' +export const isSet = (val: unknown): val is Set => + toTypeString(val) === '[object Set]' + export const isDate = (val: unknown): val is Date => val instanceof Date export const isFunction = (val: unknown): val is Function => typeof val === 'function' diff --git a/packages/shared/src/toDisplayString.ts b/packages/shared/src/toDisplayString.ts index 2c9b6ab48c7..1d0b6928400 100644 --- a/packages/shared/src/toDisplayString.ts +++ b/packages/shared/src/toDisplayString.ts @@ -1,4 +1,4 @@ -import { isArray, isObject, isPlainObject } from './index' +import { isArray, isMap, isObject, isPlainObject, isSet } from './index' /** * For converting {{ interpolation }} values to displayed strings. @@ -13,14 +13,14 @@ export const toDisplayString = (val: unknown): string => { } const replacer = (_key: string, val: any) => { - if (val instanceof Map) { + if (isMap(val)) { return { [`Map(${val.size})`]: [...val.entries()].reduce((entries, [key, val]) => { ;(entries as any)[`${key} =>`] = val return entries }, {}) } - } else if (val instanceof Set) { + } else if (isSet(val)) { return { [`Set(${val.size})`]: [...val.values()] }