BREAKING CHANGE: reactive arrays no longer unwraps contained refs When reactive arrays contain refs, especially a mix of refs and plain values, Array prototype methods will fail to function properly - e.g. sort() or reverse() will overwrite the ref's value instead of moving it (see #737). Ensuring correct behavior for all possible Array methods while retaining the ref unwrapping behavior is exceedinly complicated; In addition, even if Vue handles the built-in methods internally, it would still break when the user attempts to use a 3rd party utility functioon (e.g. lodash) on a reactive array containing refs. After this commit, similar to other collection types like Map and Set, Arrays will no longer automatically unwrap contained refs. The usage of mixed refs and plain values in Arrays should be rare in practice. In cases where this is necessary, the user can create a computed property that performs the unwrapping.
167 lines
4.4 KiB
TypeScript
167 lines
4.4 KiB
TypeScript
import { isObject, toRawType } from '@vue/shared'
|
|
import {
|
|
mutableHandlers,
|
|
readonlyHandlers,
|
|
shallowReadonlyHandlers,
|
|
shallowReactiveHandlers
|
|
} from './baseHandlers'
|
|
import {
|
|
mutableCollectionHandlers,
|
|
readonlyCollectionHandlers
|
|
} from './collectionHandlers'
|
|
import { UnwrapRef, Ref, isRef } from './ref'
|
|
import { makeMap } from '@vue/shared'
|
|
|
|
// WeakMaps that store {raw <-> observed} pairs.
|
|
const rawToReactive = new WeakMap<any, any>()
|
|
const reactiveToRaw = new WeakMap<any, any>()
|
|
const rawToReadonly = new WeakMap<any, any>()
|
|
const readonlyToRaw = new WeakMap<any, any>()
|
|
|
|
// WeakSets for values that are marked readonly or non-reactive during
|
|
// observable creation.
|
|
const readonlyValues = new WeakSet<any>()
|
|
const nonReactiveValues = new WeakSet<any>()
|
|
|
|
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
|
|
const isObservableType = /*#__PURE__*/ makeMap(
|
|
'Object,Array,Map,Set,WeakMap,WeakSet'
|
|
)
|
|
|
|
const canObserve = (value: any): boolean => {
|
|
return (
|
|
!value._isVue &&
|
|
!value._isVNode &&
|
|
isObservableType(toRawType(value)) &&
|
|
!nonReactiveValues.has(value)
|
|
)
|
|
}
|
|
|
|
// only unwrap nested ref
|
|
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
|
|
|
|
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
|
|
export function reactive(target: object) {
|
|
// if trying to observe a readonly proxy, return the readonly version.
|
|
if (readonlyToRaw.has(target)) {
|
|
return target
|
|
}
|
|
// target is explicitly marked as readonly by user
|
|
if (readonlyValues.has(target)) {
|
|
return readonly(target)
|
|
}
|
|
if (isRef(target)) {
|
|
return target
|
|
}
|
|
return createReactiveObject(
|
|
target,
|
|
rawToReactive,
|
|
reactiveToRaw,
|
|
mutableHandlers,
|
|
mutableCollectionHandlers
|
|
)
|
|
}
|
|
|
|
export function readonly<T extends object>(
|
|
target: T
|
|
): Readonly<UnwrapNestedRefs<T>> {
|
|
// value is a mutable observable, retrieve its original and return
|
|
// a readonly version.
|
|
if (reactiveToRaw.has(target)) {
|
|
target = reactiveToRaw.get(target)
|
|
}
|
|
return createReactiveObject(
|
|
target,
|
|
rawToReadonly,
|
|
readonlyToRaw,
|
|
readonlyHandlers,
|
|
readonlyCollectionHandlers
|
|
)
|
|
}
|
|
|
|
// Return a reactive-copy of the original object, where only the root level
|
|
// properties are readonly, and does NOT unwrap refs nor recursively convert
|
|
// returned properties.
|
|
// This is used for creating the props proxy object for stateful components.
|
|
export function shallowReadonly<T extends object>(
|
|
target: T
|
|
): Readonly<{ [K in keyof T]: UnwrapNestedRefs<T[K]> }> {
|
|
return createReactiveObject(
|
|
target,
|
|
rawToReadonly,
|
|
readonlyToRaw,
|
|
shallowReadonlyHandlers,
|
|
readonlyCollectionHandlers
|
|
)
|
|
}
|
|
|
|
// Return a reactive-copy of the original object, where only the root level
|
|
// properties are reactive, and does NOT unwrap refs nor recursively convert
|
|
// returned properties.
|
|
export function shallowReactive<T extends object>(target: T): T {
|
|
return createReactiveObject(
|
|
target,
|
|
rawToReactive,
|
|
reactiveToRaw,
|
|
shallowReactiveHandlers,
|
|
mutableCollectionHandlers
|
|
)
|
|
}
|
|
|
|
function createReactiveObject(
|
|
target: unknown,
|
|
toProxy: WeakMap<any, any>,
|
|
toRaw: WeakMap<any, any>,
|
|
baseHandlers: ProxyHandler<any>,
|
|
collectionHandlers: ProxyHandler<any>
|
|
) {
|
|
if (!isObject(target)) {
|
|
if (__DEV__) {
|
|
console.warn(`value cannot be made reactive: ${String(target)}`)
|
|
}
|
|
return target
|
|
}
|
|
// target already has corresponding Proxy
|
|
let observed = toProxy.get(target)
|
|
if (observed !== void 0) {
|
|
return observed
|
|
}
|
|
// target is already a Proxy
|
|
if (toRaw.has(target)) {
|
|
return target
|
|
}
|
|
// only a whitelist of value types can be observed.
|
|
if (!canObserve(target)) {
|
|
return target
|
|
}
|
|
const handlers = collectionTypes.has(target.constructor)
|
|
? collectionHandlers
|
|
: baseHandlers
|
|
observed = new Proxy(target, handlers)
|
|
toProxy.set(target, observed)
|
|
toRaw.set(observed, target)
|
|
return observed
|
|
}
|
|
|
|
export function isReactive(value: unknown): boolean {
|
|
return reactiveToRaw.has(value) || readonlyToRaw.has(value)
|
|
}
|
|
|
|
export function isReadonly(value: unknown): boolean {
|
|
return readonlyToRaw.has(value)
|
|
}
|
|
|
|
export function toRaw<T>(observed: T): T {
|
|
return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed
|
|
}
|
|
|
|
export function markReadonly<T>(value: T): T {
|
|
readonlyValues.add(value)
|
|
return value
|
|
}
|
|
|
|
export function markNonReactive<T>(value: T): T {
|
|
nonReactiveValues.add(value)
|
|
return value
|
|
}
|