diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index cf8ceae1b7d..048b7f38863 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -8,7 +8,14 @@ import { } from './reactive' import { ITERATE_KEY, MAP_KEY_ITERATE_KEY, track, trigger } from './dep' import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants' -import { capitalize, hasChanged, hasOwn, isMap, toRawType } from '@vue/shared' +import { + capitalize, + extend, + hasChanged, + hasOwn, + isMap, + toRawType, +} from '@vue/shared' import { warn } from './warning' type CollectionTypes = IterableCollections | WeakCollections @@ -23,152 +30,6 @@ const toShallow = (value: T): T => value const getProto = (v: T): any => Reflect.getPrototypeOf(v) -function get( - target: MapTypes, - key: unknown, - isReadonly = false, - isShallow = false, -) { - // #1772: readonly(reactive(Map)) should return readonly + reactive version - // of the value - target = target[ReactiveFlags.RAW] - const rawTarget = toRaw(target) - const rawKey = toRaw(key) - if (!isReadonly) { - if (hasChanged(key, rawKey)) { - track(rawTarget, TrackOpTypes.GET, key) - } - track(rawTarget, TrackOpTypes.GET, rawKey) - } - const { has } = getProto(rawTarget) - const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive - if (has.call(rawTarget, key)) { - return wrap(target.get(key)) - } else if (has.call(rawTarget, rawKey)) { - return wrap(target.get(rawKey)) - } else if (target !== rawTarget) { - // #3602 readonly(reactive(Map)) - // ensure that the nested reactive `Map` can do tracking for itself - target.get(key) - } -} - -function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean { - const target = this[ReactiveFlags.RAW] - const rawTarget = toRaw(target) - const rawKey = toRaw(key) - if (!isReadonly) { - if (hasChanged(key, rawKey)) { - track(rawTarget, TrackOpTypes.HAS, key) - } - track(rawTarget, TrackOpTypes.HAS, rawKey) - } - return key === rawKey - ? target.has(key) - : target.has(key) || target.has(rawKey) -} - -function size(target: IterableCollections, isReadonly = false) { - target = target[ReactiveFlags.RAW] - !isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) - return Reflect.get(target, 'size', target) -} - -function add(this: SetTypes, value: unknown, _isShallow = false) { - if (!_isShallow && !isShallow(value) && !isReadonly(value)) { - value = toRaw(value) - } - const target = toRaw(this) - const proto = getProto(target) - const hadKey = proto.has.call(target, value) - if (!hadKey) { - target.add(value) - trigger(target, TriggerOpTypes.ADD, value, value) - } - return this -} - -function set(this: MapTypes, key: unknown, value: unknown, _isShallow = false) { - if (!_isShallow && !isShallow(value) && !isReadonly(value)) { - value = toRaw(value) - } - const target = toRaw(this) - const { has, get } = getProto(target) - - let hadKey = has.call(target, key) - if (!hadKey) { - key = toRaw(key) - hadKey = has.call(target, key) - } else if (__DEV__) { - checkIdentityKeys(target, has, key) - } - - const oldValue = get.call(target, key) - target.set(key, value) - if (!hadKey) { - trigger(target, TriggerOpTypes.ADD, key, value) - } else if (hasChanged(value, oldValue)) { - trigger(target, TriggerOpTypes.SET, key, value, oldValue) - } - return this -} - -function deleteEntry(this: CollectionTypes, key: unknown) { - const target = toRaw(this) - const { has, get } = getProto(target) - let hadKey = has.call(target, key) - if (!hadKey) { - key = toRaw(key) - hadKey = has.call(target, key) - } else if (__DEV__) { - checkIdentityKeys(target, has, key) - } - - const oldValue = get ? get.call(target, key) : undefined - // forward the operation before queueing reactions - const result = target.delete(key) - if (hadKey) { - trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) - } - return result -} - -function clear(this: IterableCollections) { - const target = toRaw(this) - const hadItems = target.size !== 0 - const oldTarget = __DEV__ - ? isMap(target) - ? new Map(target) - : new Set(target) - : undefined - // forward the operation before queueing reactions - const result = target.clear() - if (hadItems) { - trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget) - } - return result -} - -function createForEach(isReadonly: boolean, isShallow: boolean) { - return function forEach( - this: IterableCollections, - callback: Function, - thisArg?: unknown, - ) { - const observed = this - const target = observed[ReactiveFlags.RAW] - const rawTarget = toRaw(target) - const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive - !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY) - return target.forEach((value: unknown, key: unknown) => { - // important: make sure the callback is - // 1. invoked with the reactive map as `this` and 3rd arg - // 2. the value received should be a corresponding reactive/readonly. - return callback.call(thisArg, wrap(value), wrap(key), observed) - }) - } -} - function createIterableMethod( method: string | symbol, isReadonly: boolean, @@ -232,74 +93,158 @@ function createReadonlyMethod(type: TriggerOpTypes): Function { type Instrumentations = Record -function createInstrumentations() { - const mutableInstrumentations: Instrumentations = { - get(this: MapTypes, key: unknown) { - return get(this, key) - }, - get size() { - return size(this as unknown as IterableCollections) - }, - has, - add, - set, - delete: deleteEntry, - clear, - forEach: createForEach(false, false), - } - - const shallowInstrumentations: Instrumentations = { +function createInstrumentations( + readonly: boolean, + shallow: boolean, +): Instrumentations { + const instrumentations: Instrumentations = { get(this: MapTypes, key: unknown) { - return get(this, key, false, true) + // #1772: readonly(reactive(Map)) should return readonly + reactive version + // of the value + const target = this[ReactiveFlags.RAW] + const rawTarget = toRaw(target) + const rawKey = toRaw(key) + if (!readonly) { + if (hasChanged(key, rawKey)) { + track(rawTarget, TrackOpTypes.GET, key) + } + track(rawTarget, TrackOpTypes.GET, rawKey) + } + const { has } = getProto(rawTarget) + const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive + if (has.call(rawTarget, key)) { + return wrap(target.get(key)) + } else if (has.call(rawTarget, rawKey)) { + return wrap(target.get(rawKey)) + } else if (target !== rawTarget) { + // #3602 readonly(reactive(Map)) + // ensure that the nested reactive `Map` can do tracking for itself + target.get(key) + } }, get size() { - return size(this as unknown as IterableCollections) + const target = (this as unknown as IterableCollections)[ReactiveFlags.RAW] + !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) + return Reflect.get(target, 'size', target) }, - has, - add(this: SetTypes, value: unknown) { - return add.call(this, value, true) + has(this: CollectionTypes, key: unknown): boolean { + const target = this[ReactiveFlags.RAW] + const rawTarget = toRaw(target) + const rawKey = toRaw(key) + if (!readonly) { + if (hasChanged(key, rawKey)) { + track(rawTarget, TrackOpTypes.HAS, key) + } + track(rawTarget, TrackOpTypes.HAS, rawKey) + } + return key === rawKey + ? target.has(key) + : target.has(key) || target.has(rawKey) }, - set(this: MapTypes, key: unknown, value: unknown) { - return set.call(this, key, value, true) + forEach(this: IterableCollections, callback: Function, thisArg?: unknown) { + const observed = this + const target = observed[ReactiveFlags.RAW] + const rawTarget = toRaw(target) + const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive + !readonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY) + return target.forEach((value: unknown, key: unknown) => { + // important: make sure the callback is + // 1. invoked with the reactive map as `this` and 3rd arg + // 2. the value received should be a corresponding reactive/readonly. + return callback.call(thisArg, wrap(value), wrap(key), observed) + }) }, - delete: deleteEntry, - clear, - forEach: createForEach(false, true), } - const readonlyInstrumentations: Instrumentations = { - get(this: MapTypes, key: unknown) { - return get(this, key, true) - }, - get size() { - return size(this as unknown as IterableCollections, true) - }, - has(this: MapTypes, key: unknown) { - return has.call(this, key, true) - }, - add: createReadonlyMethod(TriggerOpTypes.ADD), - set: createReadonlyMethod(TriggerOpTypes.SET), - delete: createReadonlyMethod(TriggerOpTypes.DELETE), - clear: createReadonlyMethod(TriggerOpTypes.CLEAR), - forEach: createForEach(true, false), - } + extend( + instrumentations, + readonly + ? { + add: createReadonlyMethod(TriggerOpTypes.ADD), + set: createReadonlyMethod(TriggerOpTypes.SET), + delete: createReadonlyMethod(TriggerOpTypes.DELETE), + clear: createReadonlyMethod(TriggerOpTypes.CLEAR), + } + : { + add(this: SetTypes, value: unknown) { + if (!shallow && !isShallow(value) && !isReadonly(value)) { + value = toRaw(value) + } + const target = toRaw(this) + const proto = getProto(target) + const hadKey = proto.has.call(target, value) + if (!hadKey) { + target.add(value) + trigger(target, TriggerOpTypes.ADD, value, value) + } + return this + }, + set(this: MapTypes, key: unknown, value: unknown) { + if (!shallow && !isShallow(value) && !isReadonly(value)) { + value = toRaw(value) + } + const target = toRaw(this) + const { has, get } = getProto(target) + + let hadKey = has.call(target, key) + if (!hadKey) { + key = toRaw(key) + hadKey = has.call(target, key) + } else if (__DEV__) { + checkIdentityKeys(target, has, key) + } - const shallowReadonlyInstrumentations: Instrumentations = { - get(this: MapTypes, key: unknown) { - return get(this, key, true, true) - }, - get size() { - return size(this as unknown as IterableCollections, true) - }, - has(this: MapTypes, key: unknown) { - return has.call(this, key, true) - }, - add: createReadonlyMethod(TriggerOpTypes.ADD), - set: createReadonlyMethod(TriggerOpTypes.SET), - delete: createReadonlyMethod(TriggerOpTypes.DELETE), - clear: createReadonlyMethod(TriggerOpTypes.CLEAR), - forEach: createForEach(true, true), - } + const oldValue = get.call(target, key) + target.set(key, value) + if (!hadKey) { + trigger(target, TriggerOpTypes.ADD, key, value) + } else if (hasChanged(value, oldValue)) { + trigger(target, TriggerOpTypes.SET, key, value, oldValue) + } + return this + }, + delete(this: CollectionTypes, key: unknown) { + const target = toRaw(this) + const { has, get } = getProto(target) + let hadKey = has.call(target, key) + if (!hadKey) { + key = toRaw(key) + hadKey = has.call(target, key) + } else if (__DEV__) { + checkIdentityKeys(target, has, key) + } + + const oldValue = get ? get.call(target, key) : undefined + // forward the operation before queueing reactions + const result = target.delete(key) + if (hadKey) { + trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) + } + return result + }, + clear(this: IterableCollections) { + const target = toRaw(this) + const hadItems = target.size !== 0 + const oldTarget = __DEV__ + ? isMap(target) + ? new Map(target) + : new Set(target) + : undefined + // forward the operation before queueing reactions + const result = target.clear() + if (hadItems) { + trigger( + target, + TriggerOpTypes.CLEAR, + undefined, + undefined, + oldTarget, + ) + } + return result + }, + }, + ) const iteratorMethods = [ 'keys', @@ -309,39 +254,14 @@ function createInstrumentations() { ] as const iteratorMethods.forEach(method => { - mutableInstrumentations[method] = createIterableMethod(method, false, false) - readonlyInstrumentations[method] = createIterableMethod(method, true, false) - shallowInstrumentations[method] = createIterableMethod(method, false, true) - shallowReadonlyInstrumentations[method] = createIterableMethod( - method, - true, - true, - ) + instrumentations[method] = createIterableMethod(method, readonly, shallow) }) - return [ - mutableInstrumentations, - readonlyInstrumentations, - shallowInstrumentations, - shallowReadonlyInstrumentations, - ] + return instrumentations } -const [ - mutableInstrumentations, - readonlyInstrumentations, - shallowInstrumentations, - shallowReadonlyInstrumentations, -] = /* @__PURE__*/ createInstrumentations() - function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) { - const instrumentations = shallow - ? isReadonly - ? shallowReadonlyInstrumentations - : shallowInstrumentations - : isReadonly - ? readonlyInstrumentations - : mutableInstrumentations + const instrumentations = createInstrumentations(isReadonly, shallow) return ( target: CollectionTypes,