mirror of https://github.com/vuejs/core.git
433 lines
12 KiB
TypeScript
433 lines
12 KiB
TypeScript
import { def, hasOwn, isObject, toRawType } from '@vue/shared'
|
|
import {
|
|
mutableHandlers,
|
|
readonlyHandlers,
|
|
shallowReactiveHandlers,
|
|
shallowReadonlyHandlers,
|
|
} from './baseHandlers'
|
|
import {
|
|
mutableCollectionHandlers,
|
|
readonlyCollectionHandlers,
|
|
shallowCollectionHandlers,
|
|
shallowReadonlyCollectionHandlers,
|
|
} from './collectionHandlers'
|
|
import type { RawSymbol, Ref, UnwrapRefSimple } from './ref'
|
|
import { ReactiveFlags } from './constants'
|
|
import { warn } from './warning'
|
|
|
|
export interface Target {
|
|
[ReactiveFlags.SKIP]?: boolean
|
|
[ReactiveFlags.IS_REACTIVE]?: boolean
|
|
[ReactiveFlags.IS_READONLY]?: boolean
|
|
[ReactiveFlags.IS_SHALLOW]?: boolean
|
|
[ReactiveFlags.RAW]?: any
|
|
}
|
|
|
|
export const reactiveMap: WeakMap<Target, any> = new WeakMap<Target, any>()
|
|
export const shallowReactiveMap: WeakMap<Target, any> = new WeakMap<
|
|
Target,
|
|
any
|
|
>()
|
|
export const readonlyMap: WeakMap<Target, any> = new WeakMap<Target, any>()
|
|
export const shallowReadonlyMap: WeakMap<Target, any> = new WeakMap<
|
|
Target,
|
|
any
|
|
>()
|
|
|
|
enum TargetType {
|
|
INVALID = 0,
|
|
COMMON = 1,
|
|
COLLECTION = 2,
|
|
}
|
|
|
|
function targetTypeMap(rawType: string) {
|
|
switch (rawType) {
|
|
case 'Object':
|
|
case 'Array':
|
|
return TargetType.COMMON
|
|
case 'Map':
|
|
case 'Set':
|
|
case 'WeakMap':
|
|
case 'WeakSet':
|
|
return TargetType.COLLECTION
|
|
default:
|
|
return TargetType.INVALID
|
|
}
|
|
}
|
|
|
|
function getTargetType(value: Target) {
|
|
return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
|
|
? TargetType.INVALID
|
|
: targetTypeMap(toRawType(value))
|
|
}
|
|
|
|
// only unwrap nested ref
|
|
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>
|
|
|
|
declare const ReactiveMarkerSymbol: unique symbol
|
|
|
|
export interface ReactiveMarker {
|
|
[ReactiveMarkerSymbol]?: void
|
|
}
|
|
|
|
export type Reactive<T> = UnwrapNestedRefs<T> &
|
|
(T extends readonly any[] ? ReactiveMarker : {})
|
|
|
|
/**
|
|
* Returns a reactive proxy of the object.
|
|
*
|
|
* The reactive conversion is "deep": it affects all nested properties. A
|
|
* reactive object also deeply unwraps any properties that are refs while
|
|
* maintaining reactivity.
|
|
*
|
|
* @example
|
|
* ```js
|
|
* const obj = reactive({ count: 0 })
|
|
* ```
|
|
*
|
|
* @param target - The source object.
|
|
* @see {@link https://vuejs.org/api/reactivity-core.html#reactive}
|
|
*/
|
|
export function reactive<T extends object>(target: T): Reactive<T>
|
|
export function reactive(target: object) {
|
|
// if trying to observe a readonly proxy, return the readonly version.
|
|
if (isReadonly(target)) {
|
|
return target
|
|
}
|
|
return createReactiveObject(
|
|
target,
|
|
false,
|
|
mutableHandlers,
|
|
mutableCollectionHandlers,
|
|
reactiveMap,
|
|
)
|
|
}
|
|
|
|
export declare const ShallowReactiveMarker: unique symbol
|
|
|
|
export type ShallowReactive<T> = T & { [ShallowReactiveMarker]?: true }
|
|
|
|
/**
|
|
* Shallow version of {@link reactive()}.
|
|
*
|
|
* Unlike {@link reactive()}, there is no deep conversion: only root-level
|
|
* properties are reactive for a shallow reactive object. Property values are
|
|
* stored and exposed as-is - this also means properties with ref values will
|
|
* not be automatically unwrapped.
|
|
*
|
|
* @example
|
|
* ```js
|
|
* const state = shallowReactive({
|
|
* foo: 1,
|
|
* nested: {
|
|
* bar: 2
|
|
* }
|
|
* })
|
|
*
|
|
* // mutating state's own properties is reactive
|
|
* state.foo++
|
|
*
|
|
* // ...but does not convert nested objects
|
|
* isReactive(state.nested) // false
|
|
*
|
|
* // NOT reactive
|
|
* state.nested.bar++
|
|
* ```
|
|
*
|
|
* @param target - The source object.
|
|
* @see {@link https://vuejs.org/api/reactivity-advanced.html#shallowreactive}
|
|
*/
|
|
export function shallowReactive<T extends object>(
|
|
target: T,
|
|
): ShallowReactive<T> {
|
|
return createReactiveObject(
|
|
target,
|
|
false,
|
|
shallowReactiveHandlers,
|
|
shallowCollectionHandlers,
|
|
shallowReactiveMap,
|
|
)
|
|
}
|
|
|
|
type Primitive = string | number | boolean | bigint | symbol | undefined | null
|
|
export type Builtin = Primitive | Function | Date | Error | RegExp
|
|
export type DeepReadonly<T> = T extends Builtin
|
|
? T
|
|
: T extends Map<infer K, infer V>
|
|
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
|
|
: T extends ReadonlyMap<infer K, infer V>
|
|
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
|
|
: T extends WeakMap<infer K, infer V>
|
|
? WeakMap<DeepReadonly<K>, DeepReadonly<V>>
|
|
: T extends Set<infer U>
|
|
? ReadonlySet<DeepReadonly<U>>
|
|
: T extends ReadonlySet<infer U>
|
|
? ReadonlySet<DeepReadonly<U>>
|
|
: T extends WeakSet<infer U>
|
|
? WeakSet<DeepReadonly<U>>
|
|
: T extends Promise<infer U>
|
|
? Promise<DeepReadonly<U>>
|
|
: T extends Ref<infer U, unknown>
|
|
? Readonly<Ref<DeepReadonly<U>>>
|
|
: T extends {}
|
|
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
|
|
: Readonly<T>
|
|
|
|
/**
|
|
* Takes an object (reactive or plain) or a ref and returns a readonly proxy to
|
|
* the original.
|
|
*
|
|
* A readonly proxy is deep: any nested property accessed will be readonly as
|
|
* well. It also has the same ref-unwrapping behavior as {@link reactive()},
|
|
* except the unwrapped values will also be made readonly.
|
|
*
|
|
* @example
|
|
* ```js
|
|
* const original = reactive({ count: 0 })
|
|
*
|
|
* const copy = readonly(original)
|
|
*
|
|
* watchEffect(() => {
|
|
* // works for reactivity tracking
|
|
* console.log(copy.count)
|
|
* })
|
|
*
|
|
* // mutating original will trigger watchers relying on the copy
|
|
* original.count++
|
|
*
|
|
* // mutating the copy will fail and result in a warning
|
|
* copy.count++ // warning!
|
|
* ```
|
|
*
|
|
* @param target - The source object.
|
|
* @see {@link https://vuejs.org/api/reactivity-core.html#readonly}
|
|
*/
|
|
export function readonly<T extends object>(
|
|
target: T,
|
|
): DeepReadonly<UnwrapNestedRefs<T>> {
|
|
return createReactiveObject(
|
|
target,
|
|
true,
|
|
readonlyHandlers,
|
|
readonlyCollectionHandlers,
|
|
readonlyMap,
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Shallow version of {@link readonly()}.
|
|
*
|
|
* Unlike {@link readonly()}, there is no deep conversion: only root-level
|
|
* properties are made readonly. Property values are stored and exposed as-is -
|
|
* this also means properties with ref values will not be automatically
|
|
* unwrapped.
|
|
*
|
|
* @example
|
|
* ```js
|
|
* const state = shallowReadonly({
|
|
* foo: 1,
|
|
* nested: {
|
|
* bar: 2
|
|
* }
|
|
* })
|
|
*
|
|
* // mutating state's own properties will fail
|
|
* state.foo++
|
|
*
|
|
* // ...but works on nested objects
|
|
* isReadonly(state.nested) // false
|
|
*
|
|
* // works
|
|
* state.nested.bar++
|
|
* ```
|
|
*
|
|
* @param target - The source object.
|
|
* @see {@link https://vuejs.org/api/reactivity-advanced.html#shallowreadonly}
|
|
*/
|
|
export function shallowReadonly<T extends object>(target: T): Readonly<T> {
|
|
return createReactiveObject(
|
|
target,
|
|
true,
|
|
shallowReadonlyHandlers,
|
|
shallowReadonlyCollectionHandlers,
|
|
shallowReadonlyMap,
|
|
)
|
|
}
|
|
|
|
function createReactiveObject(
|
|
target: Target,
|
|
isReadonly: boolean,
|
|
baseHandlers: ProxyHandler<any>,
|
|
collectionHandlers: ProxyHandler<any>,
|
|
proxyMap: WeakMap<Target, any>,
|
|
) {
|
|
if (!isObject(target)) {
|
|
if (__DEV__) {
|
|
warn(
|
|
`value cannot be made ${isReadonly ? 'readonly' : 'reactive'}: ${String(
|
|
target,
|
|
)}`,
|
|
)
|
|
}
|
|
return target
|
|
}
|
|
// target is already a Proxy, return it.
|
|
// exception: calling readonly() on a reactive object
|
|
if (
|
|
target[ReactiveFlags.RAW] &&
|
|
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
|
|
) {
|
|
return target
|
|
}
|
|
// only specific value types can be observed.
|
|
const targetType = getTargetType(target)
|
|
if (targetType === TargetType.INVALID) {
|
|
return target
|
|
}
|
|
// target already has corresponding Proxy
|
|
const existingProxy = proxyMap.get(target)
|
|
if (existingProxy) {
|
|
return existingProxy
|
|
}
|
|
const proxy = new Proxy(
|
|
target,
|
|
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
|
|
)
|
|
proxyMap.set(target, proxy)
|
|
return proxy
|
|
}
|
|
|
|
/**
|
|
* Checks if an object is a proxy created by {@link reactive()} or
|
|
* {@link shallowReactive()} (or {@link ref()} in some cases).
|
|
*
|
|
* @example
|
|
* ```js
|
|
* isReactive(reactive({})) // => true
|
|
* isReactive(readonly(reactive({}))) // => true
|
|
* isReactive(ref({}).value) // => true
|
|
* isReactive(readonly(ref({})).value) // => true
|
|
* isReactive(ref(true)) // => false
|
|
* isReactive(shallowRef({}).value) // => false
|
|
* isReactive(shallowReactive({})) // => true
|
|
* ```
|
|
*
|
|
* @param value - The value to check.
|
|
* @see {@link https://vuejs.org/api/reactivity-utilities.html#isreactive}
|
|
*/
|
|
export function isReactive(value: unknown): boolean {
|
|
if (isReadonly(value)) {
|
|
return isReactive((value as Target)[ReactiveFlags.RAW])
|
|
}
|
|
return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
|
|
}
|
|
|
|
/**
|
|
* Checks whether the passed value is a readonly object. The properties of a
|
|
* readonly object can change, but they can't be assigned directly via the
|
|
* passed object.
|
|
*
|
|
* The proxies created by {@link readonly()} and {@link shallowReadonly()} are
|
|
* both considered readonly, as is a computed ref without a set function.
|
|
*
|
|
* @param value - The value to check.
|
|
* @see {@link https://vuejs.org/api/reactivity-utilities.html#isreadonly}
|
|
*/
|
|
export function isReadonly(value: unknown): boolean {
|
|
return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
|
|
}
|
|
|
|
export function isShallow(value: unknown): boolean {
|
|
return !!(value && (value as Target)[ReactiveFlags.IS_SHALLOW])
|
|
}
|
|
|
|
/**
|
|
* Checks if an object is a proxy created by {@link reactive},
|
|
* {@link readonly}, {@link shallowReactive} or {@link shallowReadonly()}.
|
|
*
|
|
* @param value - The value to check.
|
|
* @see {@link https://vuejs.org/api/reactivity-utilities.html#isproxy}
|
|
*/
|
|
export function isProxy(value: any): boolean {
|
|
return value ? !!value[ReactiveFlags.RAW] : false
|
|
}
|
|
|
|
/**
|
|
* Returns the raw, original object of a Vue-created proxy.
|
|
*
|
|
* `toRaw()` can return the original object from proxies created by
|
|
* {@link reactive()}, {@link readonly()}, {@link shallowReactive()} or
|
|
* {@link shallowReadonly()}.
|
|
*
|
|
* This is an escape hatch that can be used to temporarily read without
|
|
* incurring proxy access / tracking overhead or write without triggering
|
|
* changes. It is **not** recommended to hold a persistent reference to the
|
|
* original object. Use with caution.
|
|
*
|
|
* @example
|
|
* ```js
|
|
* const foo = {}
|
|
* const reactiveFoo = reactive(foo)
|
|
*
|
|
* console.log(toRaw(reactiveFoo) === foo) // true
|
|
* ```
|
|
*
|
|
* @param observed - The object for which the "raw" value is requested.
|
|
* @see {@link https://vuejs.org/api/reactivity-advanced.html#toraw}
|
|
*/
|
|
export function toRaw<T>(observed: T): T {
|
|
const raw = observed && (observed as Target)[ReactiveFlags.RAW]
|
|
return raw ? toRaw(raw) : observed
|
|
}
|
|
|
|
export type Raw<T> = T & { [RawSymbol]?: true }
|
|
|
|
/**
|
|
* Marks an object so that it will never be converted to a proxy. Returns the
|
|
* object itself.
|
|
*
|
|
* @example
|
|
* ```js
|
|
* const foo = markRaw({})
|
|
* console.log(isReactive(reactive(foo))) // false
|
|
*
|
|
* // also works when nested inside other reactive objects
|
|
* const bar = reactive({ foo })
|
|
* console.log(isReactive(bar.foo)) // false
|
|
* ```
|
|
*
|
|
* **Warning:** `markRaw()` together with the shallow APIs such as
|
|
* {@link shallowReactive()} allow you to selectively opt-out of the default
|
|
* deep reactive/readonly conversion and embed raw, non-proxied objects in your
|
|
* state graph.
|
|
*
|
|
* @param value - The object to be marked as "raw".
|
|
* @see {@link https://vuejs.org/api/reactivity-advanced.html#markraw}
|
|
*/
|
|
export function markRaw<T extends object>(value: T): Raw<T> {
|
|
if (!hasOwn(value, ReactiveFlags.SKIP) && Object.isExtensible(value)) {
|
|
def(value, ReactiveFlags.SKIP, true)
|
|
}
|
|
return value
|
|
}
|
|
|
|
/**
|
|
* Returns a reactive proxy of the given value (if possible).
|
|
*
|
|
* If the given value is not an object, the original value itself is returned.
|
|
*
|
|
* @param value - The value for which a reactive proxy shall be created.
|
|
*/
|
|
export const toReactive = <T extends unknown>(value: T): T =>
|
|
isObject(value) ? reactive(value) : value
|
|
|
|
/**
|
|
* Returns a readonly proxy of the given value (if possible).
|
|
*
|
|
* If the given value is not an object, the original value itself is returned.
|
|
*
|
|
* @param value - The value for which a readonly proxy shall be created.
|
|
*/
|
|
export const toReadonly = <T extends unknown>(value: T): DeepReadonly<T> =>
|
|
isObject(value) ? readonly(value) : (value as DeepReadonly<T>)
|