v.value += ' World'
await nextTick()
- expect(serializeInner(root)).toBe('Hello World World World World')
- expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+ expect(serializeInner(root)).toBe('Hello World World World')
+ // 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'])
})
- // computed won't trigger compute until accessed
- c.value
+
+ it('debug: onTrigger (ref)', () => {
+ let events: DebuggerEvent[] = []
+ const onTrigger = vi.fn((e: DebuggerEvent) => {
+ events.push(e)
+ })
+ const obj = ref(1)
+ const c = computed(() => obj.value, { onTrigger })
+
- effect: c.effect,
++ // computed won't track until it has a subscriber
++ effect(() => c.value)
+
+ obj.value++
+
+ expect(c.value).toBe(2)
+ expect(onTrigger).toHaveBeenCalledTimes(1)
+ expect(events[0]).toEqual({
++ effect: c,
+ target: toRaw(obj),
+ type: TriggerOpTypes.SET,
+ key: 'value',
+ oldValue: 1,
+ newValue: 2,
+ })
+ })
})
import { pauseTracking, resetTracking } from '@vue/reactivity'
import { traverse } from './apiWatch'
-export interface DirectiveBinding<V = any> {
+export interface DirectiveBinding<
+ Value = any,
+ Modifiers extends string = string,
+ Arg extends string = string,
+> {
- instance: ComponentPublicInstance | null
+ instance: ComponentPublicInstance | Record<string, any> | null
- value: V
- oldValue: V | null
- arg?: string
- modifiers: DirectiveModifiers
- dir: ObjectDirective<any, V>
+ value: Value
+ oldValue: Value | null
+ arg?: Arg
+ modifiers: DirectiveModifiers<Modifiers>
+ dir: ObjectDirective<any, Value>
}
-export type DirectiveHook<T = any, Prev = VNode<any, T> | null, V = any> = (
- el: T,
- binding: DirectiveBinding<V>,
- vnode: VNode<any, T>,
+export type DirectiveHook<
+ HostElement = any,
+ Prev = VNode<any, HostElement> | null,
+ Value = any,
+ Modifiers extends string = string,
+ Arg extends string = string,
+> = (
+ el: HostElement,
+ binding: DirectiveBinding<Value, Modifiers, Arg>,
+ vnode: VNode<any, HostElement>,
prevVNode: Prev,
) => void