Ver código fonte

fix(reactivity): normalize toRef property keys before dep lookup + improve types (#14625)

close #12427
close #12431
edison 4 semanas atrás
pai
commit
1bb28d011b

+ 8 - 0
packages-private/dts-test/ref.test-d.ts

@@ -338,6 +338,14 @@ expectType<{ name: string } | null>(p2.union)
   // Should not distribute Refs over union
   expectType<Ref<number | string>>(toRef(obj, 'c'))
 
+  const array = reactive(['a', 'b'])
+  expectType<Ref<string>>(toRef(array, '1'))
+  expectType<Ref<string>>(toRef(array, '1', 'fallback'))
+
+  const tuple: [string, number] = ['a', 1]
+  expectType<Ref<string>>(toRef(tuple, '0'))
+  expectType<Ref<number>>(toRef(tuple, '1'))
+
   expectType<Ref<number>>(toRef(() => 123))
   expectType<Ref<number | string>>(toRef(() => obj.c))
 

+ 31 - 0
packages/reactivity/__tests__/ref.spec.ts

@@ -388,6 +388,37 @@ describe('reactivity/ref', () => {
     expect(bar.value).toBe(6)
   })
 
+  test('triggerRef on toRef created from array coerces property keys', () => {
+    const assertTriggerRef = (key: unknown) => {
+      const array = reactive(['a'])
+      const first = toRef(array as any, key as any)
+      const fn = vi.fn()
+
+      effect(() => fn(first.value))
+      expect(fn).toHaveBeenCalledTimes(1)
+
+      triggerRef(first)
+      expect(fn).toHaveBeenCalledTimes(2)
+    }
+
+    assertTriggerRef(0)
+    // JS coerces non-symbol property keys like [0] to the string "0".
+    assertTriggerRef([0])
+  })
+
+  test('triggerRef on toRef created from symbol key preserves the symbol', () => {
+    const key = Symbol()
+    const object = reactive({ [key]: 'a' })
+    const value = toRef(object, key)
+    const fn = vi.fn()
+
+    effect(() => fn(value.value))
+    expect(fn).toHaveBeenCalledTimes(1)
+
+    triggerRef(value)
+    expect(fn).toHaveBeenCalledTimes(2)
+  })
+
   test('toRef default value', () => {
     const a: { x: number | undefined } = { x: undefined }
     const x = toRef(a, 'x', 1)

+ 30 - 11
packages/reactivity/src/ref.ts

@@ -5,6 +5,7 @@ import {
   isFunction,
   isIntegerKey,
   isObject,
+  isSymbol,
 } from '@vue/shared'
 import { Dep, getDepFromReactive } from './dep'
 import {
@@ -333,6 +334,22 @@ export type ToRefs<T = any> = {
   [K in keyof T]: ToRef<T[K]>
 }
 
+type ArrayStringKey<T> = T extends readonly any[]
+  ? number extends T['length']
+    ? `${number}`
+    : never
+  : never
+
+type ToRefKey<T> = keyof T | ArrayStringKey<T>
+
+type ToRefValue<T extends object, K extends ToRefKey<T>> = K extends keyof T
+  ? T[K]
+  : T extends readonly (infer V)[]
+    ? K extends ArrayStringKey<T>
+      ? V
+      : never
+    : never
+
 /**
  * Converts a reactive object to a plain object where each property of the
  * resulting object is a ref pointing to the corresponding property of the
@@ -358,20 +375,22 @@ class ObjectRefImpl<T extends object, K extends keyof T> {
   public _value: T[K] = undefined!
 
   private readonly _raw: T
+  private readonly _key: K
   private readonly _shallow: boolean
 
   constructor(
     private readonly _object: T,
-    private readonly _key: K,
+    key: K,
     private readonly _defaultValue?: T[K],
   ) {
+    this._key = (isSymbol(key) ? key : String(key)) as K
     this._raw = toRaw(_object)
 
     let shallow = true
     let obj = _object
 
     // For an array with integer key, refs are not unwrapped
-    if (!isArray(_object) || !isIntegerKey(String(_key))) {
+    if (!isArray(_object) || isSymbol(this._key) || !isIntegerKey(this._key)) {
       // Otherwise, check each proxy layer for unwrapping
       do {
         shallow = !isProxy(obj) || isShallow(obj)
@@ -469,19 +488,19 @@ export function toRef<T>(
   : T extends Ref
     ? T
     : Ref<UnwrapRef<T>>
-export function toRef<T extends object, K extends keyof T>(
+export function toRef<T extends object, K extends ToRefKey<T>>(
   object: T,
   key: K,
-): ToRef<T[K]>
-export function toRef<T extends object, K extends keyof T>(
+): ToRef<ToRefValue<T, K>>
+export function toRef<T extends object, K extends ToRefKey<T>>(
   object: T,
   key: K,
-  defaultValue: T[K],
-): ToRef<Exclude<T[K], undefined>>
+  defaultValue: ToRefValue<T, K>,
+): ToRef<Exclude<ToRefValue<T, K>, undefined>>
 /*@__NO_SIDE_EFFECTS__*/
 export function toRef(
-  source: Record<string, any> | MaybeRef,
-  key?: string,
+  source: Record<PropertyKey, any> | MaybeRef,
+  key?: string | number | symbol,
   defaultValue?: unknown,
 ): Ref {
   if (isRef(source)) {
@@ -496,8 +515,8 @@ export function toRef(
 }
 
 function propertyToRef(
-  source: Record<string, any>,
-  key: string,
+  source: Record<PropertyKey, any>,
+  key: string | number | symbol,
   defaultValue?: unknown,
 ) {
   return new ObjectRefImpl(source, key, defaultValue) as any