360 lines
9.8 KiB
TypeScript
360 lines
9.8 KiB
TypeScript
import { toRaw, reactive, readonly, ReactiveFlags } from './reactive'
|
|
import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
|
|
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
|
import {
|
|
isObject,
|
|
capitalize,
|
|
hasOwn,
|
|
hasChanged,
|
|
toRawType,
|
|
isMap
|
|
} from '@vue/shared'
|
|
|
|
export type CollectionTypes = IterableCollections | WeakCollections
|
|
|
|
type IterableCollections = Map<any, any> | Set<any>
|
|
type WeakCollections = WeakMap<any, any> | WeakSet<any>
|
|
type MapTypes = Map<any, any> | WeakMap<any, any>
|
|
type SetTypes = Set<any> | WeakSet<any>
|
|
|
|
const toReactive = <T extends unknown>(value: T): T =>
|
|
isObject(value) ? reactive(value) : value
|
|
|
|
const toReadonly = <T extends unknown>(value: T): T =>
|
|
isObject(value) ? readonly(value as Record<any, any>) : value
|
|
|
|
const toShallow = <T extends unknown>(value: T): T => value
|
|
|
|
const getProto = <T extends CollectionTypes>(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 (key !== rawKey) {
|
|
!isReadonly && track(rawTarget, TrackOpTypes.GET, key)
|
|
}
|
|
!isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
|
|
const { has } = getProto(rawTarget)
|
|
const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive
|
|
if (has.call(rawTarget, key)) {
|
|
return wrap(target.get(key))
|
|
} else if (has.call(rawTarget, rawKey)) {
|
|
return wrap(target.get(rawKey))
|
|
}
|
|
}
|
|
|
|
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 (key !== rawKey) {
|
|
!isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
|
|
}
|
|
!isReadonly && 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 = isReadonly ? toReadonly : isShallow ? toShallow : 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)
|
|
})
|
|
}
|
|
}
|
|
|
|
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 = isReadonly ? toReadonly : isShallow ? toShallow : 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 {
|
|
// 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
|
|
}
|
|
}
|
|
|
|
const mutableInstrumentations: Record<string, Function> = {
|
|
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<string, Function> = {
|
|
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<string, Function> = {
|
|
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 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
|
|
)
|
|
})
|
|
|
|
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
|
|
const instrumentations = shallow
|
|
? 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(
|
|
hasOwn(instrumentations, key) && key in target
|
|
? instrumentations
|
|
: target,
|
|
key,
|
|
receiver
|
|
)
|
|
}
|
|
}
|
|
|
|
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
|
|
get: createInstrumentationGetter(false, false)
|
|
}
|
|
|
|
export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = {
|
|
get: createInstrumentationGetter(false, true)
|
|
}
|
|
|
|
export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
|
|
get: createInstrumentationGetter(true, false)
|
|
}
|
|
|
|
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.`
|
|
)
|
|
}
|
|
}
|