expect(serializeInner(root)).toBe('Hello World')
v.value += ' World'
- expect(c.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
await nextTick()
- expect(c.effect._dirtyLevel).toBe(
- DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
- )
expect(serializeInner(root)).toBe('Hello World World World')
- expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+ // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+ })
+
+ test('should not trigger if value did not change', () => {
+ const src = ref(0)
+ const c = computed(() => src.value % 2)
+ const spy = vi.fn()
+ effect(() => {
+ spy(c.value)
+ })
+ expect(spy).toHaveBeenCalledTimes(1)
+ src.value = 2
+
+ // should not trigger
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ src.value = 3
+ src.value = 5
+ // should trigger because latest value changes
+ expect(spy).toHaveBeenCalledTimes(2)
+ })
+
+ test('chained computed trigger', () => {
+ const effectSpy = vi.fn()
+ const c1Spy = vi.fn()
+ const c2Spy = vi.fn()
+
+ const src = ref(0)
+ const c1 = computed(() => {
+ c1Spy()
+ return src.value % 2
+ })
+ const c2 = computed(() => {
+ c2Spy()
+ return c1.value + 1
+ })
+
+ effect(() => {
+ effectSpy(c2.value)
+ })
+
+ expect(c1Spy).toHaveBeenCalledTimes(1)
+ expect(c2Spy).toHaveBeenCalledTimes(1)
+ expect(effectSpy).toHaveBeenCalledTimes(1)
+
+ src.value = 1
+ expect(c1Spy).toHaveBeenCalledTimes(2)
+ expect(c2Spy).toHaveBeenCalledTimes(2)
+ expect(effectSpy).toHaveBeenCalledTimes(2)
+ })
+
+ test('chained computed avoid re-compute', () => {
+ const effectSpy = vi.fn()
+ const c1Spy = vi.fn()
+ const c2Spy = vi.fn()
+
+ const src = ref(0)
+ const c1 = computed(() => {
+ c1Spy()
+ return src.value % 2
+ })
+ const c2 = computed(() => {
+ c2Spy()
+ return c1.value + 1
+ })
+
+ effect(() => {
+ effectSpy(c2.value)
+ })
+
+ expect(effectSpy).toHaveBeenCalledTimes(1)
+ src.value = 2
+ src.value = 4
+ src.value = 6
+ expect(c1Spy).toHaveBeenCalledTimes(4)
+ // c2 should not have to re-compute because c1 did not change.
+ expect(c2Spy).toHaveBeenCalledTimes(1)
+ // effect should not trigger because c2 did not change.
+ expect(effectSpy).toHaveBeenCalledTimes(1)
+ })
+
+ test('chained computed value invalidation', () => {
+ const effectSpy = vi.fn()
+ const c1Spy = vi.fn()
+ const c2Spy = vi.fn()
+
+ const src = ref(0)
+ const c1 = computed(() => {
+ c1Spy()
+ return src.value % 2
+ })
+ const c2 = computed(() => {
+ c2Spy()
+ return c1.value + 1
+ })
+
+ effect(() => {
+ effectSpy(c2.value)
+ })
+
+ expect(effectSpy).toHaveBeenCalledTimes(1)
+ expect(effectSpy).toHaveBeenCalledWith(1)
+ expect(c2.value).toBe(1)
+
+ expect(c1Spy).toHaveBeenCalledTimes(1)
+ expect(c2Spy).toHaveBeenCalledTimes(1)
+
+ src.value = 1
+ // value should be available sync
+ expect(c2.value).toBe(2)
+ expect(c2Spy).toHaveBeenCalledTimes(2)
+ })
+
+ test('sync access of invalidated chained computed should not prevent final effect from running', () => {
+ const effectSpy = vi.fn()
+ const c1Spy = vi.fn()
+ const c2Spy = vi.fn()
+
+ const src = ref(0)
+ const c1 = computed(() => {
+ c1Spy()
+ return src.value % 2
+ })
+ const c2 = computed(() => {
+ c2Spy()
+ return c1.value + 1
+ })
+
+ effect(() => {
+ effectSpy(c2.value)
+ })
+ expect(effectSpy).toHaveBeenCalledTimes(1)
+
+ src.value = 1
+ // sync access c2
+ c2.value
+ expect(effectSpy).toHaveBeenCalledTimes(2)
+ })
+
+ it('computed should force track in untracked zone', () => {
+ const n = ref(0)
+ const spy1 = vi.fn()
+ const spy2 = vi.fn()
+
+ let c: ComputedRef
+ effect(() => {
+ spy1()
+ pauseTracking()
+ n.value
+ c = computed(() => n.value + 1)
+ // access computed now to force refresh
+ c.value
+ effect(() => spy2(c.value))
+ n.value
+ resetTracking()
+ })
+
+ expect(spy1).toHaveBeenCalledTimes(1)
+ expect(spy2).toHaveBeenCalledTimes(1)
+
+ n.value++
+ // outer effect should not trigger
+ expect(spy1).toHaveBeenCalledTimes(1)
+ // inner effect should trigger
+ expect(spy2).toHaveBeenCalledTimes(2)
+ })
+
+ // not recommended behavior, but needed for backwards compatibility
+ // used in VueUse asyncComputed
+ it('computed side effect should be able trigger', () => {
+ const a = ref(false)
+ const b = ref(false)
+ const c = computed(() => {
+ a.value = true
+ return b.value
+ })
+ effect(() => {
+ if (a.value) {
+ b.value = true
+ }
+ })
+ expect(b.value).toBe(false)
+ // accessing c triggers change
+ c.value
+ expect(b.value).toBe(true)
+ expect(c.value).toBe(true)
+ })
+
+ it('chained computed should work when accessed before having subs', () => {
+ const n = ref(0)
+ const c = computed(() => n.value)
+ const d = computed(() => c.value + 1)
+ const spy = vi.fn()
+
+ // access
+ d.value
+
+ let dummy
+ effect(() => {
+ spy()
+ dummy = d.value
+ })
+ expect(spy).toHaveBeenCalledTimes(1)
+ expect(dummy).toBe(1)
+
+ n.value++
+ expect(spy).toHaveBeenCalledTimes(2)
+ expect(dummy).toBe(2)
+ })
+
+ // #10236
+ it('chained computed should still refresh after owner component unmount', async () => {
+ const a = ref(0)
+ const spy = vi.fn()
+
+ const Child = {
+ setup() {
+ const b = computed(() => a.value + 1)
+ const c = computed(() => b.value + 1)
+ // access
+ c.value
+ onUnmounted(() => spy(c.value))
+ return () => {}
+ },
+ }
+
+ const show = ref(true)
+ const Parent = {
+ setup() {
+ return () => (show.value ? h(Child) : null)
+ },
+ }
+
+ render(h(Parent), nodeOps.createElement('div'))
+
+ a.value++
+ show.value = false
+
+ await nextTick()
+ expect(spy).toHaveBeenCalledWith(3)
+ })
+
+ // case: radix-vue `useForwardExpose` sets a template ref during mount,
+ // and checks for the element's closest form element in a computed.
+ // the computed is expected to only evaluate after mount.
+ it('computed deps should only be refreshed when the subscribing effect is run, not when scheduled', async () => {
+ const calls: string[] = []
+ const a = ref(0)
+ const b = computed(() => {
+ calls.push('b eval')
+ return a.value + 1
+ })
+
+ const App = {
+ setup() {
+ onMounted(() => {
+ calls.push('mounted')
+ })
+ return () =>
+ h(
+ 'div',
+ {
+ ref: () => (a.value = 1),
+ },
+ b.value,
+ )
+ },
+ }
+
+ render(h(App), nodeOps.createElement('div'))
+
+ await nextTick()
+ expect(calls).toMatchObject(['b eval', 'mounted', 'b eval'])
})
- expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+ it('should chained computeds keep reactivity when computed effect happens', async () => {
+ const v = ref('Hello')
+ const c = computed(() => {
+ v.value += ' World'
+ return v.value
+ })
+ const d = computed(() => c.value)
+ const e = computed(() => d.value)
+ const Comp = {
+ setup: () => {
+ return () => d.value + ' | ' + e.value
+ },
+ }
+ const root = nodeOps.createElement('div')
+
+ render(h(Comp), root)
+ await nextTick()
+ expect(serializeInner(root)).toBe('Hello World | Hello World')
+
+ v.value += ' World'
+ await nextTick()
+ expect(serializeInner(root)).toBe(
+ 'Hello World World World | Hello World World World',
+ )
- expect(c.effect._dirtyLevel).toBe(
- DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
- )
- expect(d.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty_ComputedSideEffect)
- expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+ })
+
+ it('should keep dirty level when side effect computed value changed', () => {
+ const v = ref(0)
+ const c = computed(() => {
+ v.value += 1
+ return v.value
+ })
+ const d = computed(() => {
+ return { d: c.value }
+ })
+
+ const Comp = {
+ setup: () => {
+ return () => {
+ return [d.value.d, d.value.d]
+ }
+ },
+ }
+
+ const root = nodeOps.createElement('div')
+ render(h(Comp), root)
+
+ expect(d.value.d).toBe(1)
+ expect(serializeInner(root)).toBe('11')
+ })
+
it('debug: onTrigger (ref)', () => {
let events: DebuggerEvent[] = []
const onTrigger = vi.fn((e: DebuggerEvent) => {