فهرست منبع

wip: ref tests passing

Evan You 4 سال پیش
والد
کامیت
ac85a4217e

+ 1 - 9
src/composition-api/apiWatch.ts

@@ -8,6 +8,7 @@ import {
   isArray,
   emptyObject,
   remove,
+  hasChanged,
   isServerRendering,
   invokeWithErrorHandling
 } from 'core/util'
@@ -348,15 +349,6 @@ function doWatch(
   }
 }
 
-// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is#polyfill
-function hasChanged(x: unknown, y: unknown): boolean {
-  if (x === y) {
-    return x !== 0 || 1 / x === 1 / (y as number)
-  } else {
-    return x !== x && y !== y
-  }
-}
-
 function queuePostRenderEffect(fn: Function) {
   // TODO
 }

+ 17 - 0
src/composition-api/index.ts

@@ -17,6 +17,23 @@ export {
   CustomRefFactory
 } from './reactivity/ref'
 
+export {
+  reactive,
+  // readonly,
+  isReactive,
+  isReadonly,
+  isShallow,
+  // isProxy,
+  // shallowReactive,
+  // shallowReadonly,
+  // markRaw,
+  // toRaw,
+  ReactiveFlags,
+  // DeepReadonly,
+  // ShallowReactive,
+  UnwrapNestedRefs
+} from './reactivity/reactive'
+
 export {
   watch,
   watchEffect,

+ 35 - 3
src/composition-api/reactivity/reactive.ts

@@ -1,12 +1,44 @@
+import { observe, Observer } from 'core/observer'
+import { Ref, UnwrapRefSimple } from './ref'
+
+export const enum ReactiveFlags {
+  SKIP = '__v_skip',
+  IS_READONLY = '__v_isReadonly',
+  IS_SHALLOW = '__v_isShallow',
+  RAW = '__v_raw'
+}
+
+export interface Target {
+  __ob__?: Observer
+  [ReactiveFlags.SKIP]?: boolean
+  [ReactiveFlags.IS_READONLY]?: boolean
+  [ReactiveFlags.IS_SHALLOW]?: boolean
+  [ReactiveFlags.RAW]?: any
+}
+
 export declare const ShallowReactiveMarker: unique symbol
 
-export function reactive() {}
+// only unwrap nested ref
+export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<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 (!isReadonly(target)) {
+    observe(target)
+  }
+  return target
+}
 
 export function isReactive(value: unknown): boolean {
-  return !!(value && (value as any).__ob__)
+  return !!(value && (value as Target).__ob__)
 }
 
 export function isShallow(value: unknown): boolean {
+  return !!(value && (value as Target).__v_isShallow)
+}
+
+export function isReadonly(value: unknown): boolean {
   // TODO
-  return !!(value && (value as any).__ob__)
+  return !!(value && (value as Target).__v_isReadonly)
 }

+ 87 - 10
src/composition-api/reactivity/ref.ts

@@ -1,6 +1,8 @@
 import { defineReactive } from 'core/observer/index'
-import type { ShallowReactiveMarker } from './reactive'
+import { isReactive, ShallowReactiveMarker } from './reactive'
 import type { IfAny } from 'typescript/utils'
+import Dep from 'core/observer/dep'
+import { warn, isArray } from 'core/util'
 
 declare const RefSymbol: unique symbol
 export declare const RawSymbol: unique symbol
@@ -13,6 +15,10 @@ export interface Ref<T = any> {
    * autocomplete, so we use a private Symbol instead.
    */
   [RefSymbol]: true
+  /**
+   * @private
+   */
+  dep: Dep
 }
 
 export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
@@ -46,13 +52,13 @@ function createRef(rawValue: unknown, shallow: boolean) {
   if (isRef(rawValue)) {
     return rawValue
   }
-  const ref = { __v_isRef: true }
-  defineReactive(ref, 'value', rawValue, null, shallow)
+  const ref: any = { __v_isRef: true, __v_isShallow: shallow }
+  ref.dep = defineReactive(ref, 'value', rawValue, null, shallow)
   return ref
 }
 
 export function triggerRef(ref: Ref) {
-  // TODO triggerRefValue(ref, __DEV__ ? ref.value : void 0)
+  ref.dep.notify()
 }
 
 export function unref<T>(ref: T | Ref<T>): T {
@@ -67,22 +73,93 @@ export type CustomRefFactory<T> = (
   set: (value: T) => void
 }
 
-export function customRef() {
-  // TODO
+class CustomRefImpl<T> {
+  public dep?: Dep = undefined
+
+  private readonly _get: ReturnType<CustomRefFactory<T>>['get']
+  private readonly _set: ReturnType<CustomRefFactory<T>>['set']
+
+  public readonly __v_isRef = true
+
+  constructor(factory: CustomRefFactory<T>) {
+    const dep = new Dep()
+    const { get, set } = factory(
+      () => dep.depend(),
+      () => dep.notify()
+    )
+    this._get = get
+    this._set = set
+  }
+
+  get value() {
+    return this._get()
+  }
+
+  set value(newVal) {
+    this._set(newVal)
+  }
+}
+
+export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
+  return new CustomRefImpl(factory) as any
 }
 
 export type ToRefs<T = any> = {
   [K in keyof T]: ToRef<T[K]>
 }
 
-export function toRefs() {
-  // TODO
+export function toRefs<T extends object>(object: T): ToRefs<T> {
+  if (__DEV__ && !isReactive(object)) {
+    warn(`toRefs() expects a reactive object but received a plain one.`)
+  }
+  const ret: any = isArray(object) ? new Array(object.length) : {}
+  for (const key in object) {
+    ret[key] = toRef(object, key)
+  }
+  return ret
+}
+
+class ObjectRefImpl<T extends object, K extends keyof T> {
+  public readonly __v_isRef = true
+
+  constructor(
+    private readonly _object: T,
+    private readonly _key: K,
+    private readonly _defaultValue?: T[K]
+  ) {}
+
+  get value() {
+    const val = this._object[this._key]
+    return val === undefined ? (this._defaultValue as T[K]) : val
+  }
+
+  set value(newVal) {
+    this._object[this._key] = newVal
+  }
 }
 
 export type ToRef<T> = IfAny<T, Ref<T>, [T] extends [Ref] ? T : Ref<T>>
 
-export function toRef() {
-  // TODO
+export function toRef<T extends object, K extends keyof T>(
+  object: T,
+  key: K
+): ToRef<T[K]>
+
+export function toRef<T extends object, K extends keyof T>(
+  object: T,
+  key: K,
+  defaultValue: T[K]
+): ToRef<Exclude<T[K], undefined>>
+
+export function toRef<T extends object, K extends keyof T>(
+  object: T,
+  key: K,
+  defaultValue?: T[K]
+): ToRef<T[K]> {
+  const val = object[key]
+  return isRef(val)
+    ? val
+    : (new ObjectRefImpl(object, key, defaultValue) as any)
 }
 
 /**

+ 13 - 8
src/core/observer/index.ts

@@ -12,8 +12,10 @@ import {
   isPrimitive,
   isUndef,
   isValidArrayIndex,
-  isServerRendering
+  isServerRendering,
+  hasChanged
 } from '../util/index'
+import { isRef } from '../../composition-api'
 
 const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
 
@@ -107,7 +109,7 @@ function copyAugment(target: Object, src: Object, keys: Array<string>) {
  * or the existing observer if the value already has one.
  */
 export function observe(value: any, asRootData?: boolean): Observer | void {
-  if (!isObject(value) || value instanceof VNode) {
+  if (!isObject(value) || isRef(value) || value instanceof VNode) {
     return
   }
   let ob: Observer | void
@@ -167,22 +169,23 @@ export function defineReactive(
           }
         }
       }
-      return value
+      return isRef(value) ? value.value : value
     },
     set: function reactiveSetter(newVal) {
       const value = getter ? getter.call(obj) : val
-      /* eslint-disable no-self-compare */
-      if (newVal === value || (newVal !== newVal && value !== value)) {
+      if (!hasChanged(value, newVal)) {
         return
       }
-      /* eslint-enable no-self-compare */
       if (__DEV__ && customSetter) {
         customSetter()
       }
-      // #7981: for accessor properties without setter
-      if (getter && !setter) return
       if (setter) {
         setter.call(obj, newVal)
+      } else if (getter) {
+        // #7981: for accessor properties without setter
+        return
+      } else if (isRef(value) && !isRef(newVal)) {
+        value.value = newVal
       } else {
         val = newVal
       }
@@ -190,6 +193,8 @@ export function defineReactive(
       dep.notify()
     }
   })
+
+  return dep
 }
 
 /**

+ 9 - 0
src/shared/util.ts

@@ -349,3 +349,12 @@ export function once(fn: Function): Function {
     }
   }
 }
+
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is#polyfill
+export function hasChanged(x: unknown, y: unknown): boolean {
+  if (x === y) {
+    return x === 0 && 1 / x !== 1 / (y as number)
+  } else {
+    return x === x && y === y
+  }
+}

+ 285 - 274
test/unit/features/composition-api/reactivity/ref.spec.ts

@@ -1,6 +1,16 @@
-import { ref, shallowRef, unref } from 'vca/reactivity/ref'
+import {
+  ref,
+  isRef,
+  shallowRef,
+  unref,
+  triggerRef,
+  toRef,
+  toRefs,
+  customRef,
+  Ref
+} from 'vca/reactivity/ref'
 import { ReactiveEffect } from 'vca/reactivity/effect'
-import { isReactive } from 'vca/reactivity/reactive'
+import { isReactive, isShallow, reactive } from 'vca/reactivity/reactive'
 
 const effect = (fn: () => any) => new ReactiveEffect(fn)
 
@@ -54,59 +64,60 @@ describe('reactivity/ref', () => {
     expect(dummy).toBe(2)
   })
 
-  // it('should work like a normal property when nested in a reactive object', () => {
-  //   const a = ref(1)
-  //   const obj = reactive({
-  //     a,
-  //     b: {
-  //       c: a
-  //     }
-  //   })
-
-  //   let dummy1: number
-  //   let dummy2: number
-
-  //   effect(() => {
-  //     dummy1 = obj.a
-  //     dummy2 = obj.b.c
-  //   })
-
-  //   const assertDummiesEqualTo = (val: number) =>
-  //     [dummy1, dummy2].forEach(dummy => expect(dummy).toBe(val))
-
-  //   assertDummiesEqualTo(1)
-  //   a.value++
-  //   assertDummiesEqualTo(2)
-  //   obj.a++
-  //   assertDummiesEqualTo(3)
-  //   obj.b.c++
-  //   assertDummiesEqualTo(4)
-  // })
+  it('should work like a normal property when nested in a reactive object', () => {
+    const a = ref(1)
+    const obj = reactive({
+      a,
+      b: {
+        c: a
+      }
+    })
 
-  // it('should unwrap nested ref in types', () => {
-  //   const a = ref(0)
-  //   const b = ref(a)
+    let dummy1: number
+    let dummy2: number
 
-  //   expect(typeof (b.value + 1)).toBe('number')
-  // })
+    effect(() => {
+      dummy1 = obj.a
+      dummy2 = obj.b.c
+    })
 
-  // it('should unwrap nested values in types', () => {
-  //   const a = {
-  //     b: ref(0)
-  //   }
+    const assertDummiesEqualTo = (val: number) =>
+      [dummy1, dummy2].forEach(dummy => expect(dummy).toBe(val))
 
-  //   const c = ref(a)
+    assertDummiesEqualTo(1)
+    a.value++
+    assertDummiesEqualTo(2)
+    obj.a++
+    assertDummiesEqualTo(3)
+    obj.b.c++
+    assertDummiesEqualTo(4)
+  })
 
-  //   expect(typeof (c.value.b + 1)).toBe('number')
-  // })
+  it('should unwrap nested ref in types', () => {
+    const a = ref(0)
+    const b = ref(a)
 
-  // it('should NOT unwrap ref types nested inside arrays', () => {
-  //   const arr = ref([1, ref(3)]).value
-  //   expect(isRef(arr[0])).toBe(false)
-  //   expect(isRef(arr[1])).toBe(true)
-  //   expect((arr[1] as Ref).value).toBe(3)
-  // })
+    expect(typeof (b.value + 1)).toBe('number')
+  })
 
+  it('should unwrap nested values in types', () => {
+    const a = {
+      b: ref(0)
+    }
+
+    const c = ref(a)
+
+    expect(typeof (c.value.b + 1)).toBe('number')
+  })
+
+  it('should NOT unwrap ref types nested inside arrays', () => {
+    const arr = ref([1, ref(3)]).value
+    expect(isRef(arr[0])).toBe(false)
+    expect(isRef(arr[1])).toBe(true)
+    expect((arr[1] as Ref).value).toBe(3)
+  })
+
+  // Vue 2 does not observe array properties
   // it('should unwrap ref types as props of arrays', () => {
   //   const arr = [ref(0)]
   //   const symbolKey = Symbol('')
@@ -120,69 +131,69 @@ describe('reactivity/ref', () => {
   //   expect(arrRef[symbolKey as any]).toBe(2)
   // })
 
-  // it('should keep tuple types', () => {
-  //   const tuple: [number, string, { a: number }, () => number, Ref<number>] = [
-  //     0,
-  //     '1',
-  //     { a: 1 },
-  //     () => 0,
-  //     ref(0)
-  //   ]
-  //   const tupleRef = ref(tuple)
-
-  //   tupleRef.value[0]++
-  //   expect(tupleRef.value[0]).toBe(1)
-  //   tupleRef.value[1] += '1'
-  //   expect(tupleRef.value[1]).toBe('11')
-  //   tupleRef.value[2].a++
-  //   expect(tupleRef.value[2].a).toBe(2)
-  //   expect(tupleRef.value[3]()).toBe(0)
-  //   tupleRef.value[4].value++
-  //   expect(tupleRef.value[4].value).toBe(1)
-  // })
+  it('should keep tuple types', () => {
+    const tuple: [number, string, { a: number }, () => number, Ref<number>] = [
+      0,
+      '1',
+      { a: 1 },
+      () => 0,
+      ref(0)
+    ]
+    const tupleRef = ref(tuple)
+
+    tupleRef.value[0]++
+    expect(tupleRef.value[0]).toBe(1)
+    tupleRef.value[1] += '1'
+    expect(tupleRef.value[1]).toBe('11')
+    tupleRef.value[2].a++
+    expect(tupleRef.value[2].a).toBe(2)
+    expect(tupleRef.value[3]()).toBe(0)
+    tupleRef.value[4].value++
+    expect(tupleRef.value[4].value).toBe(1)
+  })
 
-  // it('should keep symbols', () => {
-  //   const customSymbol = Symbol()
-  //   const obj = {
-  //     [Symbol.asyncIterator]: ref(1),
-  //     [Symbol.hasInstance]: { a: ref('a') },
-  //     [Symbol.isConcatSpreadable]: { b: ref(true) },
-  //     [Symbol.iterator]: [ref(1)],
-  //     [Symbol.match]: new Set<Ref<number>>(),
-  //     [Symbol.matchAll]: new Map<number, Ref<string>>(),
-  //     [Symbol.replace]: { arr: [ref('a')] },
-  //     [Symbol.search]: { set: new Set<Ref<number>>() },
-  //     [Symbol.species]: { map: new Map<number, Ref<string>>() },
-  //     [Symbol.split]: new WeakSet<Ref<boolean>>(),
-  //     [Symbol.toPrimitive]: new WeakMap<Ref<boolean>, string>(),
-  //     [Symbol.toStringTag]: { weakSet: new WeakSet<Ref<boolean>>() },
-  //     [Symbol.unscopables]: { weakMap: new WeakMap<Ref<boolean>, string>() },
-  //     [customSymbol]: { arr: [ref(1)] }
-  //   }
-
-  //   const objRef = ref(obj)
-
-  //   const keys: (keyof typeof obj)[] = [
-  //     Symbol.asyncIterator,
-  //     Symbol.hasInstance,
-  //     Symbol.isConcatSpreadable,
-  //     Symbol.iterator,
-  //     Symbol.match,
-  //     Symbol.matchAll,
-  //     Symbol.replace,
-  //     Symbol.search,
-  //     Symbol.species,
-  //     Symbol.split,
-  //     Symbol.toPrimitive,
-  //     Symbol.toStringTag,
-  //     Symbol.unscopables,
-  //     customSymbol
-  //   ]
-
-  //   keys.forEach(key => {
-  //     expect(objRef.value[key]).toStrictEqual(obj[key])
-  //   })
-  // })
+  it('should keep symbols', () => {
+    const customSymbol = Symbol()
+    const obj = {
+      [Symbol.asyncIterator]: ref(1),
+      [Symbol.hasInstance]: { a: ref('a') },
+      [Symbol.isConcatSpreadable]: { b: ref(true) },
+      [Symbol.iterator]: [ref(1)],
+      [Symbol.match]: new Set<Ref<number>>(),
+      [Symbol.matchAll]: new Map<number, Ref<string>>(),
+      [Symbol.replace]: { arr: [ref('a')] },
+      [Symbol.search]: { set: new Set<Ref<number>>() },
+      [Symbol.species]: { map: new Map<number, Ref<string>>() },
+      [Symbol.split]: new WeakSet<Ref<boolean>>(),
+      [Symbol.toPrimitive]: new WeakMap<Ref<boolean>, string>(),
+      [Symbol.toStringTag]: { weakSet: new WeakSet<Ref<boolean>>() },
+      [Symbol.unscopables]: { weakMap: new WeakMap<Ref<boolean>, string>() },
+      [customSymbol]: { arr: [ref(1)] }
+    }
+
+    const objRef = ref(obj)
+
+    const keys: (keyof typeof obj)[] = [
+      Symbol.asyncIterator,
+      Symbol.hasInstance,
+      Symbol.isConcatSpreadable,
+      Symbol.iterator,
+      Symbol.match,
+      Symbol.matchAll,
+      Symbol.replace,
+      Symbol.search,
+      Symbol.species,
+      Symbol.split,
+      Symbol.toPrimitive,
+      Symbol.toStringTag,
+      Symbol.unscopables,
+      customSymbol
+    ]
+
+    keys.forEach(key => {
+      expect(objRef.value[key]).toStrictEqual(obj[key])
+    })
+  })
 
   test('unref', () => {
     expect(unref(1)).toBe(1)
@@ -204,192 +215,192 @@ describe('reactivity/ref', () => {
     expect(dummy).toBe(2)
   })
 
-  // test('shallowRef force trigger', () => {
-  //   const sref = shallowRef({ a: 1 })
-  //   let dummy
-  //   effect(() => {
-  //     dummy = sref.value.a
-  //   })
-  //   expect(dummy).toBe(1)
+  test('shallowRef force trigger', () => {
+    const sref = shallowRef({ a: 1 })
+    let dummy
+    effect(() => {
+      dummy = sref.value.a
+    })
+    expect(dummy).toBe(1)
 
-  //   sref.value.a = 2
-  //   expect(dummy).toBe(1) // should not trigger yet
+    sref.value.a = 2
+    expect(dummy).toBe(1) // should not trigger yet
 
-  //   // force trigger
-  //   triggerRef(sref)
-  //   expect(dummy).toBe(2)
-  // })
+    // force trigger
+    triggerRef(sref)
+    expect(dummy).toBe(2)
+  })
 
-  // test('shallowRef isShallow', () => {
-  //   expect(isShallow(shallowRef({ a: 1 }))).toBe(true)
-  // })
+  test('shallowRef isShallow', () => {
+    expect(isShallow(shallowRef({ a: 1 }))).toBe(true)
+  })
 
-  // test('isRef', () => {
-  //   expect(isRef(ref(1))).toBe(true)
-  //   expect(isRef(computed(() => 1))).toBe(true)
+  test('isRef', () => {
+    expect(isRef(ref(1))).toBe(true)
+    // TODO expect(isRef(computed(() => 1))).toBe(true)
 
-  //   expect(isRef(0)).toBe(false)
-  //   expect(isRef(1)).toBe(false)
-  //   // an object that looks like a ref isn't necessarily a ref
-  //   expect(isRef({ value: 0 })).toBe(false)
-  // })
+    expect(isRef(0)).toBe(false)
+    expect(isRef(1)).toBe(false)
+    // an object that looks like a ref isn't necessarily a ref
+    expect(isRef({ value: 0 })).toBe(false)
+  })
 
-  // test('toRef', () => {
-  //   const a = reactive({
-  //     x: 1
-  //   })
-  //   const x = toRef(a, 'x')
-  //   expect(isRef(x)).toBe(true)
-  //   expect(x.value).toBe(1)
-
-  //   // source -> proxy
-  //   a.x = 2
-  //   expect(x.value).toBe(2)
-
-  //   // proxy -> source
-  //   x.value = 3
-  //   expect(a.x).toBe(3)
-
-  //   // reactivity
-  //   let dummyX
-  //   effect(() => {
-  //     dummyX = x.value
-  //   })
-  //   expect(dummyX).toBe(x.value)
-
-  //   // mutating source should trigger effect using the proxy refs
-  //   a.x = 4
-  //   expect(dummyX).toBe(4)
-
-  //   // should keep ref
-  //   const r = { x: ref(1) }
-  //   expect(toRef(r, 'x')).toBe(r.x)
-  // })
+  test('toRef', () => {
+    const a = reactive({
+      x: 1
+    })
+    const x = toRef(a, 'x')
+    expect(isRef(x)).toBe(true)
+    expect(x.value).toBe(1)
 
-  // test('toRef default value', () => {
-  //   const a: { x: number | undefined } = { x: undefined }
-  //   const x = toRef(a, 'x', 1)
-  //   expect(x.value).toBe(1)
+    // source -> proxy
+    a.x = 2
+    expect(x.value).toBe(2)
 
-  //   a.x = 2
-  //   expect(x.value).toBe(2)
+    // proxy -> source
+    x.value = 3
+    expect(a.x).toBe(3)
 
-  //   a.x = undefined
-  //   expect(x.value).toBe(1)
-  // })
+    // reactivity
+    let dummyX
+    effect(() => {
+      dummyX = x.value
+    })
+    expect(dummyX).toBe(x.value)
 
-  // test('toRefs', () => {
-  //   const a = reactive({
-  //     x: 1,
-  //     y: 2
-  //   })
-
-  //   const { x, y } = toRefs(a)
-
-  //   expect(isRef(x)).toBe(true)
-  //   expect(isRef(y)).toBe(true)
-  //   expect(x.value).toBe(1)
-  //   expect(y.value).toBe(2)
-
-  //   // source -> proxy
-  //   a.x = 2
-  //   a.y = 3
-  //   expect(x.value).toBe(2)
-  //   expect(y.value).toBe(3)
-
-  //   // proxy -> source
-  //   x.value = 3
-  //   y.value = 4
-  //   expect(a.x).toBe(3)
-  //   expect(a.y).toBe(4)
-
-  //   // reactivity
-  //   let dummyX, dummyY
-  //   effect(() => {
-  //     dummyX = x.value
-  //     dummyY = y.value
-  //   })
-  //   expect(dummyX).toBe(x.value)
-  //   expect(dummyY).toBe(y.value)
-
-  //   // mutating source should trigger effect using the proxy refs
-  //   a.x = 4
-  //   a.y = 5
-  //   expect(dummyX).toBe(4)
-  //   expect(dummyY).toBe(5)
-  // })
+    // mutating source should trigger effect using the proxy refs
+    a.x = 4
+    expect(dummyX).toBe(4)
 
-  // test('toRefs should warn on plain object', () => {
-  //   toRefs({})
-  //   expect(`toRefs() expects a reactive object`).toHaveBeenWarned()
-  // })
+    // should keep ref
+    const r = { x: ref(1) }
+    expect(toRef(r, 'x')).toBe(r.x)
+  })
 
-  // test('toRefs should warn on plain array', () => {
-  //   toRefs([])
-  //   expect(`toRefs() expects a reactive object`).toHaveBeenWarned()
-  // })
+  test('toRef default value', () => {
+    const a: { x: number | undefined } = { x: undefined }
+    const x = toRef(a, 'x', 1)
+    expect(x.value).toBe(1)
 
-  // test('toRefs reactive array', () => {
-  //   const arr = reactive(['a', 'b', 'c'])
-  //   const refs = toRefs(arr)
+    a.x = 2
+    expect(x.value).toBe(2)
 
-  //   expect(Array.isArray(refs)).toBe(true)
+    a.x = undefined
+    expect(x.value).toBe(1)
+  })
 
-  //   refs[0].value = '1'
-  //   expect(arr[0]).toBe('1')
+  test('toRefs', () => {
+    const a = reactive({
+      x: 1,
+      y: 2
+    })
 
-  //   arr[1] = '2'
-  //   expect(refs[1].value).toBe('2')
-  // })
+    const { x, y } = toRefs(a)
 
-  // test('customRef', () => {
-  //   let value = 1
-  //   let _trigger: () => void
-
-  //   const custom = customRef((track, trigger) => ({
-  //     get() {
-  //       track()
-  //       return value
-  //     },
-  //     set(newValue: number) {
-  //       value = newValue
-  //       _trigger = trigger
-  //     }
-  //   }))
-
-  //   expect(isRef(custom)).toBe(true)
-
-  //   let dummy
-  //   effect(() => {
-  //     dummy = custom.value
-  //   })
-  //   expect(dummy).toBe(1)
-
-  //   custom.value = 2
-  //   // should not trigger yet
-  //   expect(dummy).toBe(1)
-
-  //   _trigger!()
-  //   expect(dummy).toBe(2)
-  // })
+    expect(isRef(x)).toBe(true)
+    expect(isRef(y)).toBe(true)
+    expect(x.value).toBe(1)
+    expect(y.value).toBe(2)
 
-  // test('should not trigger when setting value to same proxy', () => {
-  //   const obj = reactive({ count: 0 })
+    // source -> proxy
+    a.x = 2
+    a.y = 3
+    expect(x.value).toBe(2)
+    expect(y.value).toBe(3)
 
-  //   const a = ref(obj)
-  //   const spy1 = jest.fn(() => a.value)
+    // proxy -> source
+    x.value = 3
+    y.value = 4
+    expect(a.x).toBe(3)
+    expect(a.y).toBe(4)
 
-  //   effect(spy1)
+    // reactivity
+    let dummyX, dummyY
+    effect(() => {
+      dummyX = x.value
+      dummyY = y.value
+    })
+    expect(dummyX).toBe(x.value)
+    expect(dummyY).toBe(y.value)
+
+    // mutating source should trigger effect using the proxy refs
+    a.x = 4
+    a.y = 5
+    expect(dummyX).toBe(4)
+    expect(dummyY).toBe(5)
+  })
 
-  //   a.value = obj
-  //   expect(spy1).toBeCalledTimes(1)
+  test('toRefs should warn on plain object', () => {
+    toRefs({})
+    expect(`toRefs() expects a reactive object`).toHaveBeenWarned()
+  })
 
-  //   const b = shallowRef(obj)
-  //   const spy2 = jest.fn(() => b.value)
+  test('toRefs should warn on plain array', () => {
+    toRefs([])
+    expect(`toRefs() expects a reactive object`).toHaveBeenWarned()
+  })
 
-  //   effect(spy2)
+  test('toRefs reactive array', () => {
+    const arr = reactive(['a', 'b', 'c'])
+    const refs = toRefs(arr)
 
-  //   b.value = obj
-  //   expect(spy2).toBeCalledTimes(1)
-  // })
+    expect(Array.isArray(refs)).toBe(true)
+
+    refs[0].value = '1'
+    expect(arr[0]).toBe('1')
+
+    arr[1] = '2'
+    expect(refs[1].value).toBe('2')
+  })
+
+  test('customRef', () => {
+    let value = 1
+    let _trigger: () => void
+
+    const custom = customRef((track, trigger) => ({
+      get() {
+        track()
+        return value
+      },
+      set(newValue: number) {
+        value = newValue
+        _trigger = trigger
+      }
+    }))
+
+    expect(isRef(custom)).toBe(true)
+
+    let dummy
+    effect(() => {
+      dummy = custom.value
+    })
+    expect(dummy).toBe(1)
+
+    custom.value = 2
+    // should not trigger yet
+    expect(dummy).toBe(1)
+
+    _trigger!()
+    expect(dummy).toBe(2)
+  })
+
+  test('should not trigger when setting value to same proxy', () => {
+    const obj = reactive({ count: 0 })
+
+    const a = ref(obj)
+    const spy1 = vi.fn(() => a.value)
+
+    effect(spy1)
+
+    a.value = obj
+    expect(spy1).toBeCalledTimes(1)
+
+    const b = shallowRef(obj)
+    const spy2 = vi.fn(() => b.value)
+
+    effect(spy2)
+
+    b.value = obj
+    expect(spy2).toBeCalledTimes(1)
+  })
 })