import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from "./constants"; import { toDeepReactive, toDeepReadonly, toRaw } from "./reactivity"; import { ITERATE_KEY, MAP_KEY_ITERATE_KEY, track, trigger } from "./tracking"; import { capitalize, hasChanged, hasOwn, isMap, toRawType } from "./utils"; export type CollectionTypes = IterableCollections | WeakCollections; type IterableCollections = Map | Set; type WeakCollections = WeakMap | WeakSet; type MapTypes = Map | WeakMap; type SetTypes = Set | WeakSet; 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 as any)[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 ? toDeepReadonly : toDeepReactive; 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 as any)[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 as any)[ReactiveFlags.RAW]; !isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY); return Reflect.get(target, "size", target); } function add(this: SetTypes, value: unknown) { 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) { 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 as any; const target = observed[ReactiveFlags.RAW]; const rawTarget = toRaw(target); const wrap = isShallow ? toShallow : isReadonly ? toDeepReadonly : toDeepReactive; !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); }); }; } interface Iterable { [Symbol.iterator](): Iterator; } interface Iterator { next(value?: any): IterationResult; } interface IterationResult { value: any; done: boolean; } function createIterableMethod( method: string | symbol, isReadonly: boolean, isShallow: boolean ) { return function ( this: IterableCollections, ...args: unknown[] ): Iterable & Iterator { const target = (this as any)[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 ? toDeepReadonly : toDeepReactive; !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 { // iterator protocol next() { const { value, done } = innerIterator.next(); return done ? { value, done } : { value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), done, }; }, // iterable protocol [Symbol.iterator]() { return this; }, }; }; } function createReadonlyMethod(type: TriggerOpTypes): Function { return function (this: CollectionTypes, ...args: unknown[]) { if (__DEV__) { const key = args[0] ? `on key "${args[0]}" ` : ``; console.warn( `${capitalize(type)} operation ${key}failed: target is readonly.`, toRaw(this) ); } return type === TriggerOpTypes.DELETE ? false : this; }; } function createInstrumentations() { const mutableInstrumentations: Record = { 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: Record = { get(this: MapTypes, key: unknown) { return get(this, key, false, true); }, get size() { return size(this as unknown as IterableCollections); }, has, add, set, delete: deleteEntry, clear, forEach: createForEach(false, true), }; const readonlyInstrumentations: Record = { 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), }; const shallowReadonlyInstrumentations: Record = { 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 iteratorMethods = ["keys", "values", "entries", Symbol.iterator]; iteratorMethods.forEach((method) => { mutableInstrumentations[method as string] = createIterableMethod( method, false, false ); readonlyInstrumentations[method as string] = createIterableMethod( method, true, false ); shallowInstrumentations[method as string] = createIterableMethod( method, false, true ); shallowReadonlyInstrumentations[method as string] = createIterableMethod( method, true, true ); }); return [ mutableInstrumentations, readonlyInstrumentations, shallowInstrumentations, shallowReadonlyInstrumentations, ]; } const [ mutableInstrumentations, readonlyInstrumentations, shallowInstrumentations, shallowReadonlyInstrumentations, ] = /* #__PURE__*/ createInstrumentations(); function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) { const instrumentations = shallow ? isReadonly ? shallowReadonlyInstrumentations : shallowInstrumentations : isReadonly ? readonlyInstrumentations : mutableInstrumentations; 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.RAW) { return target; } return Reflect.get( // @ts-expect-error 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); console.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.` ); } }