diff --git a/src/composition-api/apiWatch.ts b/src/composition-api/apiWatch.ts index faa524406..59862b1dd 100644 --- a/src/composition-api/apiWatch.ts +++ b/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 } diff --git a/src/composition-api/index.ts b/src/composition-api/index.ts index 428e6cf6a..2163d905c 100644 --- a/src/composition-api/index.ts +++ b/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, diff --git a/src/composition-api/reactivity/reactive.ts b/src/composition-api/reactivity/reactive.ts index 60fc9886a..1cd310a85 100644 --- a/src/composition-api/reactivity/reactive.ts +++ b/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 extends Ref ? T : UnwrapRefSimple + +export function reactive(target: T): UnwrapNestedRefs +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 { - // TODO - return !!(value && (value as any).__ob__) + return !!(value && (value as Target).__v_isShallow) +} + +export function isReadonly(value: unknown): boolean { + // TODO + return !!(value && (value as Target).__v_isReadonly) } diff --git a/src/composition-api/reactivity/ref.ts b/src/composition-api/reactivity/ref.ts index c4a5f28f4..960b198ba 100644 --- a/src/composition-api/reactivity/ref.ts +++ b/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 { * autocomplete, so we use a private Symbol instead. */ [RefSymbol]: true + /** + * @private + */ + dep: Dep } export function isRef(r: Ref | unknown): r is Ref @@ -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(ref: T | Ref): T { @@ -67,22 +73,93 @@ export type CustomRefFactory = ( set: (value: T) => void } -export function customRef() { - // TODO +class CustomRefImpl { + public dep?: Dep = undefined + + private readonly _get: ReturnType>['get'] + private readonly _set: ReturnType>['set'] + + public readonly __v_isRef = true + + constructor(factory: CustomRefFactory) { + 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(factory: CustomRefFactory): Ref { + return new CustomRefImpl(factory) as any } export type ToRefs = { [K in keyof T]: ToRef } -export function toRefs() { - // TODO +export function toRefs(object: T): ToRefs { + 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 { + 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 = IfAny, [T] extends [Ref] ? T : Ref> -export function toRef() { - // TODO +export function toRef( + object: T, + key: K +): ToRef + +export function toRef( + object: T, + key: K, + defaultValue: T[K] +): ToRef> + +export function toRef( + object: T, + key: K, + defaultValue?: T[K] +): ToRef { + const val = object[key] + return isRef(val) + ? val + : (new ObjectRefImpl(object, key, defaultValue) as any) } /** diff --git a/src/core/observer/index.ts b/src/core/observer/index.ts index 1a534c061..8a2da69e1 100644 --- a/src/core/observer/index.ts +++ b/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) { * 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 } /** diff --git a/src/shared/util.ts b/src/shared/util.ts index f4497057a..fa2761171 100644 --- a/src/shared/util.ts +++ b/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 + } +} diff --git a/test/unit/features/composition-api/reactivity/ref.spec.ts b/test/unit/features/composition-api/reactivity/ref.spec.ts index 984547bba..d696348e2 100644 --- a/test/unit/features/composition-api/reactivity/ref.spec.ts +++ b/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 - // } - // }) + 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 + let dummy1: number + let dummy2: number - // effect(() => { - // dummy1 = obj.a - // dummy2 = obj.b.c - // }) + effect(() => { + dummy1 = obj.a + dummy2 = obj.b.c + }) - // const assertDummiesEqualTo = (val: number) => - // [dummy1, dummy2].forEach(dummy => expect(dummy).toBe(val)) + 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) - // }) + assertDummiesEqualTo(1) + a.value++ + assertDummiesEqualTo(2) + obj.a++ + assertDummiesEqualTo(3) + obj.b.c++ + assertDummiesEqualTo(4) + }) - // it('should unwrap nested ref in types', () => { - // const a = ref(0) - // const b = ref(a) + it('should unwrap nested ref in types', () => { + const a = ref(0) + const b = ref(a) - // expect(typeof (b.value + 1)).toBe('number') - // }) + expect(typeof (b.value + 1)).toBe('number') + }) - // it('should unwrap nested values in types', () => { - // const a = { - // b: ref(0) - // } + it('should unwrap nested values in types', () => { + const a = { + b: ref(0) + } - // const c = ref(a) + const c = ref(a) - // expect(typeof (c.value.b + 1)).toBe('number') - // }) + 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) - // }) + 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] = [ - // 0, - // '1', - // { a: 1 }, - // () => 0, - // ref(0) - // ] - // const tupleRef = ref(tuple) + it('should keep tuple types', () => { + const tuple: [number, string, { a: number }, () => number, Ref] = [ + 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) - // }) + 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>(), - // [Symbol.matchAll]: new Map>(), - // [Symbol.replace]: { arr: [ref('a')] }, - // [Symbol.search]: { set: new Set>() }, - // [Symbol.species]: { map: new Map>() }, - // [Symbol.split]: new WeakSet>(), - // [Symbol.toPrimitive]: new WeakMap, string>(), - // [Symbol.toStringTag]: { weakSet: new WeakSet>() }, - // [Symbol.unscopables]: { weakMap: new WeakMap, string>() }, - // [customSymbol]: { arr: [ref(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>(), + [Symbol.matchAll]: new Map>(), + [Symbol.replace]: { arr: [ref('a')] }, + [Symbol.search]: { set: new Set>() }, + [Symbol.species]: { map: new Map>() }, + [Symbol.split]: new WeakSet>(), + [Symbol.toPrimitive]: new WeakMap, string>(), + [Symbol.toStringTag]: { weakSet: new WeakSet>() }, + [Symbol.unscopables]: { weakMap: new WeakMap, string>() }, + [customSymbol]: { arr: [ref(1)] } + } - // const objRef = ref(obj) + 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 - // ] + 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]) - // }) - // }) + 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) + 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) + // source -> proxy + a.x = 2 + expect(x.value).toBe(2) - // // proxy -> source - // x.value = 3 - // expect(a.x).toBe(3) + // proxy -> source + x.value = 3 + expect(a.x).toBe(3) - // // reactivity - // let dummyX - // effect(() => { - // dummyX = x.value - // }) - // expect(dummyX).toBe(x.value) + // 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) + // 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) - // }) + // should keep ref + const r = { x: ref(1) } + expect(toRef(r, 'x')).toBe(r.x) + }) - // test('toRef default value', () => { - // const a: { x: number | undefined } = { x: undefined } - // const x = toRef(a, 'x', 1) - // 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) - // a.x = 2 - // expect(x.value).toBe(2) + a.x = 2 + expect(x.value).toBe(2) - // a.x = undefined - // expect(x.value).toBe(1) - // }) + a.x = undefined + expect(x.value).toBe(1) + }) - // test('toRefs', () => { - // const a = reactive({ - // x: 1, - // y: 2 - // }) + test('toRefs', () => { + const a = reactive({ + x: 1, + y: 2 + }) - // const { x, y } = toRefs(a) + 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) + 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) + // 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) + // 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) + // 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 + a.y = 5 + expect(dummyX).toBe(4) + expect(dummyY).toBe(5) + }) - // test('toRefs should warn on plain object', () => { - // toRefs({}) - // expect(`toRefs() expects a reactive object`).toHaveBeenWarned() - // }) + test('toRefs should warn on plain object', () => { + toRefs({}) + expect(`toRefs() expects a reactive object`).toHaveBeenWarned() + }) - // test('toRefs should warn on plain array', () => { - // toRefs([]) - // expect(`toRefs() expects a reactive object`).toHaveBeenWarned() - // }) + test('toRefs should warn on plain array', () => { + toRefs([]) + expect(`toRefs() expects a reactive object`).toHaveBeenWarned() + }) - // test('toRefs reactive array', () => { - // const arr = reactive(['a', 'b', 'c']) - // const refs = toRefs(arr) + test('toRefs reactive array', () => { + const arr = reactive(['a', 'b', 'c']) + const refs = toRefs(arr) - // expect(Array.isArray(refs)).toBe(true) + expect(Array.isArray(refs)).toBe(true) - // refs[0].value = '1' - // expect(arr[0]).toBe('1') + refs[0].value = '1' + expect(arr[0]).toBe('1') - // arr[1] = '2' - // expect(refs[1].value).toBe('2') - // }) + arr[1] = '2' + expect(refs[1].value).toBe('2') + }) - // test('customRef', () => { - // let value = 1 - // let _trigger: () => void + test('customRef', () => { + let value = 1 + let _trigger: () => void - // const custom = customRef((track, trigger) => ({ - // get() { - // track() - // return value - // }, - // set(newValue: number) { - // value = newValue - // _trigger = trigger - // } - // })) + const custom = customRef((track, trigger) => ({ + get() { + track() + return value + }, + set(newValue: number) { + value = newValue + _trigger = trigger + } + })) - // expect(isRef(custom)).toBe(true) + expect(isRef(custom)).toBe(true) - // let dummy - // effect(() => { - // dummy = custom.value - // }) - // expect(dummy).toBe(1) + let dummy + effect(() => { + dummy = custom.value + }) + expect(dummy).toBe(1) - // custom.value = 2 - // // should not trigger yet - // expect(dummy).toBe(1) + custom.value = 2 + // should not trigger yet + expect(dummy).toBe(1) - // _trigger!() - // expect(dummy).toBe(2) - // }) + _trigger!() + expect(dummy).toBe(2) + }) - // test('should not trigger when setting value to same proxy', () => { - // const obj = reactive({ count: 0 }) + test('should not trigger when setting value to same proxy', () => { + const obj = reactive({ count: 0 }) - // const a = ref(obj) - // const spy1 = jest.fn(() => a.value) + const a = ref(obj) + const spy1 = vi.fn(() => a.value) - // effect(spy1) + effect(spy1) - // a.value = obj - // expect(spy1).toBeCalledTimes(1) + a.value = obj + expect(spy1).toBeCalledTimes(1) - // const b = shallowRef(obj) - // const spy2 = jest.fn(() => b.value) + const b = shallowRef(obj) + const spy2 = vi.fn(() => b.value) - // effect(spy2) + effect(spy2) - // b.value = obj - // expect(spy2).toBeCalledTimes(1) - // }) + b.value = obj + expect(spy2).toBeCalledTimes(1) + }) })