refactor: preserve refs in reactive arrays

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.
This commit is contained in:
Evan You
2020-02-21 17:48:39 +01:00
parent 627b9df4a2
commit 775a7c2b41
6 changed files with 137 additions and 93 deletions

View File

@@ -16,20 +16,21 @@ const shallowReactiveGet = /*#__PURE__*/ createGetter(false, true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)
const arrayIdentityInstrumentations: Record<string, Function> = {}
const arrayInstrumentations: Record<string, Function> = {}
;['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
arrayIdentityInstrumentations[key] = function(
value: unknown,
...args: any[]
): any {
return toRaw(this)[key](toRaw(value), ...args)
arrayInstrumentations[key] = function(...args: any[]): any {
const arr = toRaw(this) as any
for (let i = 0, l = (this as any).length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
return arr[key](...args.map(toRaw))
}
})
function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
if (isArray(target) && hasOwn(arrayIdentityInstrumentations, key)) {
return Reflect.get(arrayIdentityInstrumentations, key, receiver)
if (isArray(target) && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) && builtInSymbols.has(key)) {
@@ -40,7 +41,8 @@ function createGetter(isReadonly = false, shallow = false) {
// TODO strict mode that returns a shallow-readonly version of the value
return res
}
if (isRef(res)) {
// ref unwrapping, only for Objects, not for Arrays.
if (isRef(res) && !isArray(target)) {
return res.value
}
track(target, TrackOpTypes.GET, key)
@@ -79,7 +81,7 @@ function createSetter(isReadonly = false, shallow = false) {
const oldValue = (target as any)[key]
if (!shallow) {
value = toRaw(value)
if (isRef(oldValue) && !isRef(value)) {
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
@@ -94,6 +96,7 @@ function createSetter(isReadonly = false, shallow = false) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
debugger
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}

View File

@@ -9,7 +9,7 @@ import {
mutableCollectionHandlers,
readonlyCollectionHandlers
} from './collectionHandlers'
import { UnwrapRef, Ref } from './ref'
import { UnwrapRef, Ref, isRef } from './ref'
import { makeMap } from '@vue/shared'
// WeakMaps that store {raw <-> observed} pairs.
@@ -50,6 +50,9 @@ export function reactive(target: object) {
if (readonlyValues.has(target)) {
return readonly(target)
}
if (isRef(target)) {
return target
}
return createReactiveObject(
target,
rawToReactive,

View File

@@ -29,7 +29,6 @@ export function isRef(r: any): r is Ref {
}
export function ref<T>(value: T): T extends Ref ? T : Ref<T>
export function ref<T>(value: T): Ref<T>
export function ref<T = any>(): Ref<T>
export function ref(value?: unknown) {
if (isRef(value)) {
@@ -83,8 +82,6 @@ function toProxyRef<T extends object, K extends keyof T>(
} as any
}
type UnwrapArray<T> = { [P in keyof T]: UnwrapRef<T[P]> }
// corner case when use narrows type
// Ex. type RelativePath = string & { __brand: unknown }
// RelativePath extends object -> true
@@ -94,7 +91,7 @@ type BaseTypes = string | number | boolean
export type UnwrapRef<T> = {
cRef: T extends ComputedRef<infer V> ? UnwrapRef<V> : T
ref: T extends Ref<infer V> ? UnwrapRef<V> : T
array: T extends Array<infer V> ? Array<UnwrapRef<V>> & UnwrapArray<T> : T
array: T
object: { [K in keyof T]: UnwrapRef<T[K]> }
}[T extends ComputedRef<any>
? 'cRef'