import { type Target, isReadonly, isShallow, toRaw, toReactive, toReadonly, } from './reactive' import { ITERATE_KEY, MAP_KEY_ITERATE_KEY, track, trigger } from './dep' import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants' import { capitalize, extend, hasChanged, hasOwn, isMap, toRawType, } from '@vue/shared' import { warn } from './warning' type CollectionTypes = IterableCollections | WeakCollections type IterableCollections = (Map | Set) & Target type WeakCollections = (WeakMap | WeakSet) & Target type MapTypes = (Map | WeakMap) & Target type SetTypes = (Set | WeakSet) & Target const toShallow = (value: T): T => value const getProto = (v: T): any => Reflect.getPrototypeOf(v) function createIterableMethod( method: string | symbol, isReadonly: boolean, isShallow: boolean, ) { return function ( this: IterableCollections, ...args: unknown[] ): Iterable & Iterator { const target = this[ReactiveFlags.RAW] const rawTarget = toRaw(target) const targetIsMap = isMap(rawTarget) const isPair = method === 'entries' || (method === Symbol.iterator && targetIsMap) const isKeyOnly = method === 'keys' && targetIsMap const innerIterator = target[method](...args) const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive !isReadonly && track( rawTarget, TrackOpTypes.ITERATE, isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY, ) // return a wrapped iterator which returns observed versions of the // values emitted from the real iterator return extend( // inheriting all iterator properties Object.create(innerIterator), { // iterator protocol next() { const { value, done } = innerIterator.next() return done ? { value, done } : { value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), done, } }, }, ) } } function createReadonlyMethod(type: TriggerOpTypes, method?: string): Function { const isGetOrInsertOp = method === 'getOrInsert' || method === 'getOrInsertComputed' return function (this: CollectionTypes, ...args: unknown[]) { const target = toRaw(this) const key = args[0] if (isGetOrInsertOp) { const { has } = getProto(target) const hadKey = has.call(target, key) || has.call(target, toRaw(key)) if (hadKey) { return (this as MapTypes).get(key) } } if (__DEV__) { const keyInfo = key ? `on key "${key}" ` : `` const methodInfo = isGetOrInsertOp ? `(method: ${method}()) ` : '' warn( `${capitalize(type)} operation ${methodInfo}${keyInfo}failed: target is readonly.`, target, ) } return isGetOrInsertOp ? undefined : type === TriggerOpTypes.DELETE ? false : type === TriggerOpTypes.CLEAR ? undefined : this } } type Instrumentations = Record function createInstrumentations( readonly: boolean, shallow: boolean, ): Instrumentations { const instrumentations: Instrumentations = { get(this: MapTypes, key: unknown) { // #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() { const target = (this as unknown as IterableCollections)[ReactiveFlags.RAW] !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY) return target.size }, 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) }, 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) }) }, } extend( instrumentations, readonly ? { add: createReadonlyMethod(TriggerOpTypes.ADD), set: createReadonlyMethod(TriggerOpTypes.SET), getOrInsert: createReadonlyMethod(TriggerOpTypes.SET, 'getOrInsert'), getOrInsertComputed: createReadonlyMethod( TriggerOpTypes.SET, 'getOrInsertComputed', ), delete: createReadonlyMethod(TriggerOpTypes.DELETE), clear: createReadonlyMethod(TriggerOpTypes.CLEAR), } : { add(this: SetTypes, value: unknown) { const target = toRaw(this) const proto = getProto(target) const rawValue = toRaw(value) const valueToAdd = !shallow && !isShallow(value) && !isReadonly(value) ? rawValue : value const hadKey = proto.has.call(target, valueToAdd) || (hasChanged(value, valueToAdd) && proto.has.call(target, value)) || (hasChanged(rawValue, valueToAdd) && proto.has.call(target, rawValue)) if (!hadKey) { target.add(valueToAdd) trigger(target, TriggerOpTypes.ADD, valueToAdd, valueToAdd) } 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 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 }, getOrInsert(this: MapTypes, key: unknown, defaultValue: unknown) { const target = toRaw(this) const { has } = getProto(target) const hadKey = has.call(target, key) || has.call(target, toRaw(key)) if (!hadKey) { this.set(key, defaultValue) } return this.get(key) }, getOrInsertComputed( this: MapTypes, key: unknown, callback: (key: unknown) => unknown, ) { const target = toRaw(this) const { has } = getProto(target) const hadKey = has.call(target, key) || has.call(target, toRaw(key)) if (!hadKey) { this.set(key, callback(key)) } return this.get(key) }, 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', 'values', 'entries', Symbol.iterator, ] as const iteratorMethods.forEach(method => { instrumentations[method] = createIterableMethod(method, readonly, shallow) }) return instrumentations } function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) { const instrumentations = createInstrumentations(isReadonly, shallow) return ( target: CollectionTypes, key: string | symbol, receiver: CollectionTypes, ) => { if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { return isReadonly } else if (key === ReactiveFlags.IS_SHALLOW) { return shallow } else if (key === ReactiveFlags.RAW) { return target } return Reflect.get( hasOwn(instrumentations, key) && key in target ? instrumentations : target, key, receiver, ) } } export const mutableCollectionHandlers: ProxyHandler = { get: /*@__PURE__*/ createInstrumentationGetter(false, false), } export const shallowCollectionHandlers: ProxyHandler = { get: /*@__PURE__*/ createInstrumentationGetter(false, true), } export const readonlyCollectionHandlers: ProxyHandler = { get: /*@__PURE__*/ createInstrumentationGetter(true, false), } export const shallowReadonlyCollectionHandlers: ProxyHandler = { get: /*@__PURE__*/ createInstrumentationGetter(true, true), } function checkIdentityKeys( target: CollectionTypes, has: (key: unknown) => boolean, key: unknown, ) { const rawKey = toRaw(key) if (rawKey !== key && has.call(target, rawKey)) { const type = toRawType(target) warn( `Reactive ${type} contains both the raw and reactive ` + `versions of the same object${type === `Map` ? ` as keys` : ``}, ` + `which can lead to inconsistencies. ` + `Avoid differentiating between the raw and reactive versions ` + `of an object and only use the reactive version if possible.`, ) } }