--- /dev/null
+import { bench, describe } from 'vitest'
+import { type ComputedRef, type Ref, computed, effect, ref } from '../src'
+
+describe('computed', () => {
+ bench('create computed', () => {
+ computed(() => 100)
+ })
+
+ {
+ const v = ref(100)
+ computed(() => v.value * 2)
+ let i = 0
+ bench("write ref, don't read computed (without effect)", () => {
+ v.value = i++
+ })
+ }
+
+ {
+ const v = ref(100)
+ const c = computed(() => {
+ return v.value * 2
+ })
+ effect(() => c.value)
+ let i = 0
+ bench("write ref, don't read computed (with effect)", () => {
+ v.value = i++
+ })
+ }
+
+ {
+ const v = ref(100)
+ const c = computed(() => {
+ return v.value * 2
+ })
+ let i = 0
+ bench('write ref, read computed (without effect)', () => {
+ v.value = i++
+ c.value
+ })
+ }
+
+ {
+ const v = ref(100)
+ const c = computed(() => {
+ return v.value * 2
+ })
+ effect(() => c.value)
+ let i = 0
+ bench('write ref, read computed (with effect)', () => {
+ v.value = i++
+ c.value
+ })
+ }
+
+ {
+ const v = ref(100)
+ const computeds: ComputedRef<number>[] = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ const c = computed(() => {
+ return v.value * 2
+ })
+ computeds.push(c)
+ }
+ let i = 0
+ bench("write ref, don't read 1000 computeds (without effect)", () => {
+ v.value = i++
+ })
+ }
+
+ {
+ const v = ref(100)
+ const computeds: ComputedRef<number>[] = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ const c = computed(() => {
+ return v.value * 2
+ })
+ effect(() => c.value)
+ computeds.push(c)
+ }
+ let i = 0
+ bench(
+ "write ref, don't read 1000 computeds (with multiple effects)",
+ () => {
+ v.value = i++
+ },
+ )
+ }
+
+ {
+ const v = ref(100)
+ const computeds: ComputedRef<number>[] = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ const c = computed(() => {
+ return v.value * 2
+ })
+ computeds.push(c)
+ }
+ effect(() => {
+ for (let i = 0; i < 1000; i++) {
+ computeds[i].value
+ }
+ })
+ let i = 0
+ bench("write ref, don't read 1000 computeds (with single effect)", () => {
+ v.value = i++
+ })
+ }
+
+ {
+ const v = ref(100)
+ const computeds: ComputedRef<number>[] = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ const c = computed(() => {
+ return v.value * 2
+ })
+ computeds.push(c)
+ }
+ let i = 0
+ bench('write ref, read 1000 computeds (no effect)', () => {
+ v.value = i++
+ computeds.forEach(c => c.value)
+ })
+ }
+
+ {
+ const v = ref(100)
+ const computeds: ComputedRef<number>[] = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ const c = computed(() => {
+ return v.value * 2
+ })
+ effect(() => c.value)
+ computeds.push(c)
+ }
+ let i = 0
+ bench('write ref, read 1000 computeds (with multiple effects)', () => {
+ v.value = i++
+ computeds.forEach(c => c.value)
+ })
+ }
+
+ {
+ const v = ref(100)
+ const computeds: ComputedRef<number>[] = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ const c = computed(() => {
+ return v.value * 2
+ })
+ effect(() => c.value)
+ computeds.push(c)
+ }
+ effect(() => {
+ for (let i = 0; i < 1000; i++) {
+ computeds[i].value
+ }
+ })
+ let i = 0
+ bench('write ref, read 1000 computeds (with single effect)', () => {
+ v.value = i++
+ computeds.forEach(c => c.value)
+ })
+ }
+
+ {
+ const refs: Ref<number>[] = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ refs.push(ref(i))
+ }
+ const c = computed(() => {
+ let total = 0
+ refs.forEach(ref => (total += ref.value))
+ return total
+ })
+ let i = 0
+ const n = refs.length
+ bench('1000 refs, read 1 computed (without effect)', () => {
+ refs[i++ % n].value++
+ c.value
+ })
+ }
+
+ {
+ const refs: Ref<number>[] = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ refs.push(ref(i))
+ }
+ const c = computed(() => {
+ let total = 0
+ refs.forEach(ref => (total += ref.value))
+ return total
+ })
+ effect(() => c.value)
+ let i = 0
+ const n = refs.length
+ bench('1000 refs, read 1 computed (with effect)', () => {
+ refs[i++ % n].value++
+ c.value
+ })
+ }
+})
--- /dev/null
+import { bench, describe } from 'vitest'
+import { type Ref, effect, ref } from '../src'
+
+describe('effect', () => {
+ {
+ let i = 0
+ const n = ref(0)
+ effect(() => n.value)
+ bench('single ref invoke', () => {
+ n.value = i++
+ })
+ }
+
+ function benchEffectCreate(size: number) {
+ bench(`create an effect that tracks ${size} refs`, () => {
+ const refs: Ref[] = []
+ for (let i = 0; i < size; i++) {
+ refs.push(ref(i))
+ }
+ effect(() => {
+ for (let i = 0; i < size; i++) {
+ refs[i].value
+ }
+ })
+ })
+ }
+
+ benchEffectCreate(1)
+ benchEffectCreate(10)
+ benchEffectCreate(100)
+ benchEffectCreate(1000)
+
+ function benchEffectCreateAndStop(size: number) {
+ bench(`create and stop an effect that tracks ${size} refs`, () => {
+ const refs: Ref[] = []
+ for (let i = 0; i < size; i++) {
+ refs.push(ref(i))
+ }
+ const e = effect(() => {
+ for (let i = 0; i < size; i++) {
+ refs[i].value
+ }
+ })
+ e.effect.stop()
+ })
+ }
+
+ benchEffectCreateAndStop(1)
+ benchEffectCreateAndStop(10)
+ benchEffectCreateAndStop(100)
+ benchEffectCreateAndStop(1000)
+
+ function benchWithRefs(size: number) {
+ let j = 0
+ const refs: Ref[] = []
+ for (let i = 0; i < size; i++) {
+ refs.push(ref(i))
+ }
+ effect(() => {
+ for (let i = 0; i < size; i++) {
+ refs[i].value
+ }
+ })
+ bench(`1 effect, mutate ${size} refs`, () => {
+ for (let i = 0; i < size; i++) {
+ refs[i].value = i + j++
+ }
+ })
+ }
+
+ benchWithRefs(10)
+ benchWithRefs(100)
+ benchWithRefs(1000)
+
+ function benchWithBranches(size: number) {
+ const toggle = ref(true)
+ const refs: Ref[] = []
+ for (let i = 0; i < size; i++) {
+ refs.push(ref(i))
+ }
+ effect(() => {
+ if (toggle.value) {
+ for (let i = 0; i < size; i++) {
+ refs[i].value
+ }
+ }
+ })
+ bench(`${size} refs branch toggle`, () => {
+ toggle.value = !toggle.value
+ })
+ }
+
+ benchWithBranches(10)
+ benchWithBranches(100)
+ benchWithBranches(1000)
+
+ function benchMultipleEffects(size: number) {
+ let i = 0
+ const n = ref(0)
+ for (let i = 0; i < size; i++) {
+ effect(() => n.value)
+ }
+ bench(`1 ref invoking ${size} effects`, () => {
+ n.value = i++
+ })
+ }
+
+ benchMultipleEffects(10)
+ benchMultipleEffects(100)
+ benchMultipleEffects(1000)
+})
for (let amount = 1e1; amount < 1e4; amount *= 10) {
{
- const rawArray = []
+ const rawArray: any[] = []
for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i)
}
}
{
- const rawArray = []
+ const rawArray: any[] = []
for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i)
}
}
{
- const rawArray = []
+ const rawArray: any[] = []
for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i)
}
}
{
- const rawArray = []
+ const rawArray: any[] = []
for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i)
}
{
const r = reactive(createMap({ a: 1 }))
- const computeds = []
+ const computeds: any[] = []
for (let i = 0, n = 1000; i < n; i++) {
const c = computed(() => {
return r.get('a') * 2
{
const r = reactive(createMap({ a: 1 }))
- const computeds = []
+ const computeds: any[] = []
for (let i = 0, n = 1000; i < n; i++) {
const c = computed(() => {
return r.get('a') * 2
--- /dev/null
+import { bench } from 'vitest'
+import { reactive } from '../src'
+
+bench('create reactive obj', () => {
+ reactive({ a: 1 })
+})
+
+{
+ const r = reactive({ a: 1 })
+ bench('read reactive obj property', () => {
+ r.a
+ })
+}
+
+{
+ let i = 0
+ const r = reactive({ a: 1 })
+ bench('write reactive obj property', () => {
+ r.a = i++
+ })
+}
const v = ref(100)
bench('write/read ref', () => {
v.value = i++
-
v.value
})
}
+++ /dev/null
-import { bench, describe } from 'vitest'
-import { type ComputedRef, type Ref, computed, ref } from '../src/index'
-
-describe('computed', () => {
- bench('create computed', () => {
- computed(() => 100)
- })
-
- {
- let i = 0
- const o = ref(100)
- bench('write independent ref dep', () => {
- o.value = i++
- })
- }
-
- {
- const v = ref(100)
- computed(() => v.value * 2)
- let i = 0
- bench("write ref, don't read computed (never invoked)", () => {
- v.value = i++
- })
- }
-
- {
- const v = ref(100)
- computed(() => {
- return v.value * 2
- })
- let i = 0
- bench("write ref, don't read computed (never invoked)", () => {
- v.value = i++
- })
- }
-
- {
- const v = ref(100)
- const c = computed(() => {
- return v.value * 2
- })
- c.value
- let i = 0
- bench("write ref, don't read computed (invoked)", () => {
- v.value = i++
- })
- }
-
- {
- const v = ref(100)
- const c = computed(() => {
- return v.value * 2
- })
- let i = 0
- bench('write ref, read computed', () => {
- v.value = i++
- c.value
- })
- }
-
- {
- const v = ref(100)
- const computeds = []
- for (let i = 0, n = 1000; i < n; i++) {
- const c = computed(() => {
- return v.value * 2
- })
- computeds.push(c)
- }
- let i = 0
- bench("write ref, don't read 1000 computeds (never invoked)", () => {
- v.value = i++
- })
- }
-
- {
- const v = ref(100)
- const computeds = []
- for (let i = 0, n = 1000; i < n; i++) {
- const c = computed(() => {
- return v.value * 2
- })
- c.value
- computeds.push(c)
- }
- let i = 0
- bench("write ref, don't read 1000 computeds (invoked)", () => {
- v.value = i++
- })
- }
-
- {
- const v = ref(100)
- const computeds: ComputedRef<number>[] = []
- for (let i = 0, n = 1000; i < n; i++) {
- const c = computed(() => {
- return v.value * 2
- })
- c.value
- computeds.push(c)
- }
- let i = 0
- bench('write ref, read 1000 computeds', () => {
- v.value = i++
- computeds.forEach(c => c.value)
- })
- }
-
- {
- const refs: Ref<number>[] = []
- for (let i = 0, n = 1000; i < n; i++) {
- refs.push(ref(i))
- }
- const c = computed(() => {
- let total = 0
- refs.forEach(ref => (total += ref.value))
- return total
- })
- let i = 0
- const n = refs.length
- bench('1000 refs, 1 computed', () => {
- refs[i++ % n].value++
- c.value
- })
- }
-})
-import { h, nextTick, nodeOps, render, serializeInner } from '@vue/runtime-test'
+import {
+ h,
+ nextTick,
+ nodeOps,
+ onMounted,
+ onUnmounted,
+ render,
+ serializeInner,
+} from '@vue/runtime-test'
import {
type DebuggerEvent,
ITERATE_KEY,
shallowRef,
toRaw,
} from '../src'
-import { DirtyLevels } from '../src/constants'
-import { COMPUTED_SIDE_EFFECT_WARN } from '../src/computed'
+import { EffectFlags, pauseTracking, resetTracking } from '../src/effect'
+import type { ComputedRef, ComputedRefImpl } from '../src/computed'
describe('reactivity/computed', () => {
it('should return updated value', () => {
expect(getter2).toHaveBeenCalledTimes(2)
})
- it('should no longer update when stopped', () => {
- const value = reactive<{ foo?: number }>({})
- const cValue = computed(() => value.foo)
- let dummy
- effect(() => {
- dummy = cValue.value
- })
- expect(dummy).toBe(undefined)
- value.foo = 1
- expect(dummy).toBe(1)
- cValue.effect.stop()
- value.foo = 2
- expect(dummy).toBe(1)
- })
-
it('should support setter', () => {
const n = ref(1)
const plusOne = computed({
expect(isReadonly(z.value.a)).toBe(false)
})
- it('should expose value when stopped', () => {
- const x = computed(() => 1)
- x.effect.stop()
- expect(x.value).toBe(1)
- })
-
it('debug: onTrack', () => {
let events: DebuggerEvent[] = []
const onTrack = vi.fn((e: DebuggerEvent) => {
expect(onTrack).toHaveBeenCalledTimes(3)
expect(events).toEqual([
{
- effect: c.effect,
+ effect: c,
target: toRaw(obj),
type: TrackOpTypes.GET,
key: 'foo',
},
{
- effect: c.effect,
+ effect: c,
target: toRaw(obj),
type: TrackOpTypes.HAS,
key: 'bar',
},
{
- effect: c.effect,
+ effect: c,
target: toRaw(obj),
type: TrackOpTypes.ITERATE,
key: ITERATE_KEY,
const obj = reactive<{ foo?: number }>({ foo: 1 })
const c = computed(() => obj.foo, { onTrigger })
- // computed won't trigger compute until accessed
- c.value
+ // computed won't track until it has a subscriber
+ effect(() => c.value)
obj.foo!++
expect(c.value).toBe(2)
expect(onTrigger).toHaveBeenCalledTimes(1)
expect(events[0]).toEqual({
- effect: c.effect,
+ effect: c,
target: toRaw(obj),
type: TriggerOpTypes.SET,
key: 'foo',
expect(c.value).toBeUndefined()
expect(onTrigger).toHaveBeenCalledTimes(2)
expect(events[1]).toEqual({
- effect: c.effect,
+ effect: c,
target: toRaw(obj),
type: TriggerOpTypes.DELETE,
key: 'foo',
const a = ref(0)
const b = computed(() => {
return a.value % 3 !== 0
- })
+ }) as unknown as ComputedRefImpl
const c = computed(() => {
cSpy()
if (a.value % 3 === 2) {
return 'expensive'
}
return 'cheap'
- })
+ }) as unknown as ComputedRefImpl
const d = computed(() => {
return a.value % 3 === 2
- })
+ }) as unknown as ComputedRefImpl
const e = computed(() => {
if (b.value) {
if (d.value) {
}
}
return c.value
- })
+ }) as unknown as ComputedRefImpl
e.value
a.value++
e.value
- expect(e.effect.deps.length).toBe(3)
- expect(e.effect.deps.indexOf((b as any).dep)).toBe(0)
- expect(e.effect.deps.indexOf((d as any).dep)).toBe(1)
- expect(e.effect.deps.indexOf((c as any).dep)).toBe(2)
+ expect(e.deps!.dep).toBe(b.dep)
+ expect(e.deps!.nextDep!.dep).toBe(d.dep)
+ expect(e.deps!.nextDep!.nextDep!.dep).toBe(c.dep)
expect(cSpy).toHaveBeenCalledTimes(2)
a.value++
expect(fnSpy).toBeCalledTimes(2)
})
- it('should chained recurse effects clear dirty after trigger', () => {
+ it('should chained recursive effects clear dirty after trigger', () => {
const v = ref(1)
- const c1 = computed(() => v.value)
- const c2 = computed(() => c1.value)
+ const c1 = computed(() => v.value) as unknown as ComputedRefImpl
+ const c2 = computed(() => c1.value) as unknown as ComputedRefImpl
- c1.effect.allowRecurse = true
- c2.effect.allowRecurse = true
c2.value
-
- expect(c1.effect._dirtyLevel).toBe(DirtyLevels.NotDirty)
- expect(c2.effect._dirtyLevel).toBe(DirtyLevels.NotDirty)
+ expect(c1.flags & EffectFlags.DIRTY).toBeFalsy()
+ expect(c2.flags & EffectFlags.DIRTY).toBeFalsy()
})
it('should chained computeds dirtyLevel update with first computed effect', () => {
const c3 = computed(() => c2.value)
c3.value
-
- expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
- expect(c2.effect._dirtyLevel).toBe(
- DirtyLevels.MaybeDirty_ComputedSideEffect,
- )
- expect(c3.effect._dirtyLevel).toBe(
- DirtyLevels.MaybeDirty_ComputedSideEffect,
- )
- expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+ // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
})
it('should work when chained(ref+computed)', () => {
})
const c2 = computed(() => v.value + c1.value)
expect(c2.value).toBe('0foo')
- expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
expect(c2.value).toBe('1foo')
- expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+ // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
})
it('should trigger effect even computed already dirty', () => {
const c2 = computed(() => v.value + c1.value)
effect(() => {
- fnSpy()
- c2.value
+ fnSpy(c2.value)
})
expect(fnSpy).toBeCalledTimes(1)
- expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
- expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
+ expect(fnSpy.mock.calls).toMatchObject([['0foo']])
+ expect(v.value).toBe(1)
v.value = 2
expect(fnSpy).toBeCalledTimes(2)
- expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+ expect(fnSpy.mock.calls).toMatchObject([['0foo'], ['2foo']])
+ expect(v.value).toBe(2)
+ // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
})
// #10185
c3.value
v2.value = true
- expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
- expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
c3.value
- expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
- expect(c2.effect._dirtyLevel).toBe(
- DirtyLevels.MaybeDirty_ComputedSideEffect,
- )
- expect(c3.effect._dirtyLevel).toBe(
- DirtyLevels.MaybeDirty_ComputedSideEffect,
- )
-
v1.value.v.value = 999
- expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
- expect(c2.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
- expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
expect(c3.value).toBe('yes')
- expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+ // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
})
it('should be not dirty after deps mutate (mutate deps in computed)', async () => {
await nextTick()
await nextTick()
expect(serializeInner(root)).toBe(`2`)
- expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+ // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
})
- it('should not trigger effect scheduler by recurse computed effect', async () => {
+ it('should not trigger effect scheduler by recursive computed effect', async () => {
const v = ref('Hello')
const c = computed(() => {
v.value += ' World'
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'])
})
})
+++ /dev/null
-import { computed, effect, ref } from '../src'
-
-describe('deferred computed', () => {
- 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)
- })
-
- test('should not compute if deactivated before scheduler is called', () => {
- const c1Spy = vi.fn()
- const src = ref(0)
- const c1 = computed(() => {
- c1Spy()
- return src.value % 2
- })
- effect(() => c1.value)
- expect(c1Spy).toHaveBeenCalledTimes(1)
-
- c1.effect.stop()
- // trigger
- src.value++
- expect(c1Spy).toHaveBeenCalledTimes(1)
- })
-})
stop,
toRaw,
} from '../src/index'
-import { pauseScheduling, resetScheduling } from '../src/effect'
-import { ITERATE_KEY, getDepFromReactive } from '../src/reactiveEffect'
+import { type Dep, ITERATE_KEY, getDepFromReactive } from '../src/dep'
import {
computed,
h,
render,
serializeInner,
} from '@vue/runtime-test'
+import {
+ endBatch,
+ pauseTracking,
+ resetTracking,
+ startBatch,
+} from '../src/effect'
describe('reactivity/effect', () => {
it('should run the passed function once (wrapped by a effect)', () => {
expect(dummy).toBe(1)
})
- it('lazy', () => {
- const obj = reactive({ foo: 1 })
- let dummy
- const runner = effect(() => (dummy = obj.foo), { lazy: true })
- expect(dummy).toBe(undefined)
-
- expect(runner()).toBe(1)
- expect(dummy).toBe(1)
- obj.foo = 2
- expect(dummy).toBe(2)
- })
-
it('scheduler', () => {
let dummy
let run: any
})
})
- it('should be triggered once with pauseScheduling', () => {
+ it('should be triggered once with batching', () => {
const counter = reactive({ num: 0 })
const counterSpy = vi.fn(() => counter.num)
counterSpy.mockClear()
- pauseScheduling()
+ startBatch()
counter.num++
counter.num++
- resetScheduling()
+ endBatch()
expect(counterSpy).toHaveBeenCalledTimes(1)
})
expect(renderSpy).toHaveBeenCalledTimes(2)
})
- describe('empty dep cleanup', () => {
+ it('nested effect should force track in untracked zone', () => {
+ const n = ref(0)
+ const spy1 = vi.fn()
+ const spy2 = vi.fn()
+
+ effect(() => {
+ spy1()
+ pauseTracking()
+ n.value
+ effect(() => {
+ n.value
+ spy2()
+ })
+ 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)
+ })
+
+ describe('dep unsubscribe', () => {
+ function getSubCount(dep: Dep | undefined) {
+ let count = 0
+ let sub = dep!.subs
+ while (sub) {
+ count++
+ sub = sub.prevSub
+ }
+ return count
+ }
+
it('should remove the dep when the effect is stopped', () => {
const obj = reactive({ prop: 1 })
- expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
const runner = effect(() => obj.prop)
const dep = getDepFromReactive(toRaw(obj), 'prop')
- expect(dep).toHaveLength(1)
+ expect(getSubCount(dep)).toBe(1)
obj.prop = 2
- expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
- expect(dep).toHaveLength(1)
+ expect(getSubCount(dep)).toBe(1)
stop(runner)
- expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ expect(getSubCount(dep)).toBe(0)
obj.prop = 3
runner()
- expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ expect(getSubCount(dep)).toBe(0)
})
it('should only remove the dep when the last effect is stopped', () => {
const obj = reactive({ prop: 1 })
- expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
const runner1 = effect(() => obj.prop)
const dep = getDepFromReactive(toRaw(obj), 'prop')
- expect(dep).toHaveLength(1)
+ expect(getSubCount(dep)).toBe(1)
const runner2 = effect(() => obj.prop)
- expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
- expect(dep).toHaveLength(2)
+ expect(getSubCount(dep)).toBe(2)
obj.prop = 2
- expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
- expect(dep).toHaveLength(2)
+ expect(getSubCount(dep)).toBe(2)
stop(runner1)
- expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
- expect(dep).toHaveLength(1)
+ expect(getSubCount(dep)).toBe(1)
obj.prop = 3
- expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
- expect(dep).toHaveLength(1)
+ expect(getSubCount(dep)).toBe(1)
stop(runner2)
- expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
obj.prop = 4
runner1()
runner2()
- expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ expect(getSubCount(dep)).toBe(0)
})
it('should remove the dep when it is no longer used by the effect', () => {
b: 2,
c: 'a',
})
- expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
effect(() => obj[obj.c])
const depC = getDepFromReactive(toRaw(obj), 'c')
- expect(getDepFromReactive(toRaw(obj), 'a')).toHaveLength(1)
- expect(getDepFromReactive(toRaw(obj), 'b')).toBeUndefined()
- expect(depC).toHaveLength(1)
+ expect(getSubCount(getDepFromReactive(toRaw(obj), 'a'))).toBe(1)
+ expect(getSubCount(depC)).toBe(1)
obj.c = 'b'
obj.a = 4
- expect(getDepFromReactive(toRaw(obj), 'a')).toBeUndefined()
- expect(getDepFromReactive(toRaw(obj), 'b')).toHaveLength(1)
+ expect(getSubCount(getDepFromReactive(toRaw(obj), 'b'))).toBe(1)
expect(getDepFromReactive(toRaw(obj), 'c')).toBe(depC)
- expect(depC).toHaveLength(1)
+ expect(getSubCount(depC)).toBe(1)
})
})
})
watchEffect(() => {
watchEffectSpy()
r.value
+ c.value
})
})
- c!.value // computed is lazy so trigger collection
expect(computedSpy).toHaveBeenCalledTimes(1)
expect(watchSpy).toHaveBeenCalledTimes(0)
expect(watchEffectSpy).toHaveBeenCalledTimes(1)
r.value++
- c!.value
await nextTick()
expect(computedSpy).toHaveBeenCalledTimes(2)
expect(watchSpy).toHaveBeenCalledTimes(1)
scope.stop()
r.value++
- c!.value
await nextTick()
// should not trigger anymore
expect(computedSpy).toHaveBeenCalledTimes(2)
shallowRef as ref,
toRaw,
} from '../src/index'
-import { getDepFromReactive } from '../src/reactiveEffect'
+import { getDepFromReactive } from '../src/dep'
describe.skipIf(!global.gc)('reactivity/gc', () => {
const gc = () => {
+++ /dev/null
-import { bench } from 'vitest'
-import { type ComputedRef, computed, reactive } from '../src'
-
-bench('create reactive obj', () => {
- reactive({ a: 1 })
-})
-
-{
- let i = 0
- const r = reactive({ a: 1 })
- bench('write reactive obj property', () => {
- r.a = i++
- })
-}
-
-{
- const r = reactive({ a: 1 })
- computed(() => {
- return r.a * 2
- })
- let i = 0
- bench("write reactive obj, don't read computed (never invoked)", () => {
- r.a = i++
- })
-}
-
-{
- const r = reactive({ a: 1 })
- const c = computed(() => {
- return r.a * 2
- })
- c.value
- let i = 0
- bench("write reactive obj, don't read computed (invoked)", () => {
- r.a = i++
- })
-}
-
-{
- const r = reactive({ a: 1 })
- const c = computed(() => {
- return r.a * 2
- })
- let i = 0
- bench('write reactive obj, read computed', () => {
- r.a = i++
- c.value
- })
-}
-
-{
- const r = reactive({ a: 1 })
- const computeds = []
- for (let i = 0, n = 1000; i < n; i++) {
- const c = computed(() => {
- return r.a * 2
- })
- computeds.push(c)
- }
- let i = 0
- bench("write reactive obj, don't read 1000 computeds (never invoked)", () => {
- r.a = i++
- })
-}
-
-{
- const r = reactive({ a: 1 })
- const computeds = []
- for (let i = 0, n = 1000; i < n; i++) {
- const c = computed(() => {
- return r.a * 2
- })
- c.value
- computeds.push(c)
- }
- let i = 0
- bench("write reactive obj, don't read 1000 computeds (invoked)", () => {
- r.a = i++
- })
-}
-
-{
- const r = reactive({ a: 1 })
- const computeds: ComputedRef<number>[] = []
- for (let i = 0, n = 1000; i < n; i++) {
- const c = computed(() => {
- return r.a * 2
- })
- computeds.push(c)
- }
- let i = 0
- bench('write reactive obj, read 1000 computeds', () => {
- r.a = i++
- computeds.forEach(c => c.value)
- })
-}
-
-{
- const reactives: Record<string, number>[] = []
- for (let i = 0, n = 1000; i < n; i++) {
- reactives.push(reactive({ a: i }))
- }
- const c = computed(() => {
- let total = 0
- reactives.forEach(r => (total += r.a))
- return total
- })
- let i = 0
- const n = reactives.length
- bench('1000 reactive objs, 1 computed', () => {
- reactives[i++ % n].a++
- c.value
- })
-}
const eff = effect(() => {
roArr.includes(2)
})
- expect(eff.effect.deps.length).toBe(0)
+ expect(eff.effect.deps).toBeUndefined()
})
test('readonly should track and trigger if wrapping reactive original (collection)', () => {
expect(a.value).toBe(rr)
expect(a.value).not.toBe(r)
})
+
+ test('should not trigger when setting the same raw object', () => {
+ const obj = {}
+ const r = ref(obj)
+ const spy = vi.fn()
+ effect(() => spy(r.value))
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ r.value = obj
+ expect(spy).toHaveBeenCalledTimes(1)
+ })
})
shallowArray.pop()
expect(size).toBe(0)
})
+
test('should not observe when iterating', () => {
const shallowArray = shallowReactive<object[]>([])
const a = {}
toRaw,
} from './reactive'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
-import {
- pauseScheduling,
- pauseTracking,
- resetScheduling,
- resetTracking,
-} from './effect'
-import { ITERATE_KEY, track, trigger } from './reactiveEffect'
+import { ITERATE_KEY, track, trigger } from './dep'
import {
hasChanged,
hasOwn,
} from '@vue/shared'
import { isRef } from './ref'
import { warn } from './warning'
+import { endBatch, pauseTracking, resetTracking, startBatch } from './effect'
const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`)
// which leads to infinite loops in some cases (#2137)
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
+ startBatch()
pauseTracking()
- pauseScheduling()
const res = (toRaw(this) as any)[key].apply(this, args)
- resetScheduling()
resetTracking()
+ endBatch()
return res
}
})
}
}
- const res = Reflect.get(target, key, receiver)
+ const res = Reflect.get(
+ target,
+ key,
+ // if this is a proxy wrapping a ref, return methods using the raw ref
+ // as receiver so that we don't have to call `toRaw` on the ref in all
+ // its class methods
+ isRef(target) ? target : receiver,
+ )
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
import { toRaw, toReactive, toReadonly } from './reactive'
-import {
- ITERATE_KEY,
- MAP_KEY_ITERATE_KEY,
- track,
- trigger,
-} from './reactiveEffect'
+import { ITERATE_KEY, MAP_KEY_ITERATE_KEY, track, trigger } from './dep'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { capitalize, hasChanged, hasOwn, isMap, toRawType } from '@vue/shared'
-import { type DebuggerOptions, ReactiveEffect } from './effect'
-import { type Ref, trackRefValue, triggerRefValue } from './ref'
-import { NOOP, hasChanged, isFunction } from '@vue/shared'
-import { toRaw } from './reactive'
-import type { Dep } from './dep'
-import { DirtyLevels, ReactiveFlags } from './constants'
+import { isFunction } from '@vue/shared'
+import {
+ type DebuggerEvent,
+ type DebuggerOptions,
+ EffectFlags,
+ type Link,
+ type ReactiveEffect,
+ type Subscriber,
+ activeSub,
+ refreshComputed,
+} from './effect'
+import type { Ref } from './ref'
import { warn } from './warning'
+import { Dep, globalVersion } from './dep'
+import { ReactiveFlags, TrackOpTypes } from './constants'
declare const ComputedRefSymbol: unique symbol
}
export interface WritableComputedRef<T> extends Ref<T> {
- readonly effect: ReactiveEffect<T>
+ /**
+ * @deprecated computed no longer uses effect
+ */
+ effect: ReactiveEffect
}
export type ComputedGetter<T> = (oldValue?: T) => T
set: ComputedSetter<T>
}
-export const COMPUTED_SIDE_EFFECT_WARN =
- `Computed is still dirty after getter evaluation,` +
- ` likely because a computed is mutating its own dependency in its getter.` +
- ` State mutations in computed getters should be avoided. ` +
- ` Check the docs for more details: https://vuejs.org/guide/essentials/computed.html#getters-should-be-side-effect-free`
-
-export class ComputedRefImpl<T> {
- public dep?: Dep = undefined
-
- private _value!: T
- public readonly effect: ReactiveEffect<T>
-
- public readonly __v_isRef = true
- public readonly [ReactiveFlags.IS_READONLY]: boolean = false
-
- public _cacheable: boolean
+/**
+ * @internal
+ */
+export class ComputedRefImpl<T = any> implements Subscriber {
+ // A computed is a ref
+ _value: any = undefined
+ readonly dep = new Dep(this)
+ readonly __v_isRef = true;
+ readonly [ReactiveFlags.IS_READONLY]: boolean
+ // A computed is also a subscriber that tracks other deps
+ deps?: Link = undefined
+ depsTail?: Link = undefined
+ // track variaous states
+ flags = EffectFlags.DIRTY
+ // last seen global version
+ globalVersion = globalVersion - 1
+ // for backwards compat
+ effect = this
+
+ // dev only
+ onTrack?: (event: DebuggerEvent) => void
+ // dev only
+ onTrigger?: (event: DebuggerEvent) => void
constructor(
- getter: ComputedGetter<T>,
- private readonly _setter: ComputedSetter<T>,
- isReadonly: boolean,
- isSSR: boolean,
+ public fn: ComputedGetter<T>,
+ private readonly setter: ComputedSetter<T> | undefined,
+ public isSSR: boolean,
) {
- this.effect = new ReactiveEffect(
- () => getter(this._value),
- () =>
- triggerRefValue(
- this,
- this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect
- ? DirtyLevels.MaybeDirty_ComputedSideEffect
- : DirtyLevels.MaybeDirty,
- ),
- )
- this.effect.computed = this
- this.effect.active = this._cacheable = !isSSR
- this[ReactiveFlags.IS_READONLY] = isReadonly
+ this.__v_isReadonly = !setter
}
- get value() {
- // the computed ref may get wrapped by other proxies e.g. readonly() #3376
- const self = toRaw(this)
- if (
- (!self._cacheable || self.effect.dirty) &&
- hasChanged(self._value, (self._value = self.effect.run()!))
- ) {
- triggerRefValue(self, DirtyLevels.Dirty)
+ notify() {
+ // avoid infinite self recursion
+ if (activeSub !== this) {
+ this.flags |= EffectFlags.DIRTY
+ this.dep.notify()
+ } else if (__DEV__) {
+ // TODO warn
}
- trackRefValue(self)
- if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
- __DEV__ && warn(COMPUTED_SIDE_EFFECT_WARN)
- triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
- }
- return self._value
- }
-
- set value(newValue: T) {
- this._setter(newValue)
}
- // #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x
- get _dirty() {
- return this.effect.dirty
+ get value() {
+ const link = __DEV__
+ ? this.dep.track({
+ target: this,
+ type: TrackOpTypes.GET,
+ key: 'value',
+ })
+ : this.dep.track()
+ refreshComputed(this)
+ // sync version after evaluation
+ if (link) {
+ link.version = this.dep.version
+ }
+ return this._value
}
- set _dirty(v) {
- this.effect.dirty = v
+ set value(newValue) {
+ if (this.setter) {
+ this.setter(newValue)
+ } else if (__DEV__) {
+ warn('Write operation failed: computed value is readonly')
+ }
}
- // #endregion
}
/**
isSSR = false,
) {
let getter: ComputedGetter<T>
- let setter: ComputedSetter<T>
+ let setter: ComputedSetter<T> | undefined
- const onlyGetter = isFunction(getterOrOptions)
- if (onlyGetter) {
+ if (isFunction(getterOrOptions)) {
getter = getterOrOptions
- setter = __DEV__
- ? () => {
- warn('Write operation failed: computed value is readonly')
- }
- : NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
- const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
+ const cRef = new ComputedRefImpl(getter, setter, isSSR)
if (__DEV__ && debugOptions && !isSSR) {
- cRef.effect.onTrack = debugOptions.onTrack
- cRef.effect.onTrigger = debugOptions.onTrigger
+ cRef.onTrack = debugOptions.onTrack
+ cRef.onTrigger = debugOptions.onTrigger
}
return cRef as any
+++ /dev/null
-import { computed } from './computed'
-
-/**
- * @deprecated use `computed` instead. See #5912
- */
-export const deferredComputed = computed
-import type { ReactiveEffect } from './effect'
+import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
import type { ComputedRefImpl } from './computed'
+import { type TrackOpTypes, TriggerOpTypes } from './constants'
+import {
+ type DebuggerEventExtraInfo,
+ EffectFlags,
+ type Link,
+ activeSub,
+ endBatch,
+ shouldTrack,
+ startBatch,
+} from './effect'
-export type Dep = Map<ReactiveEffect, number> & {
- cleanup: () => void
- computed?: ComputedRefImpl<any>
+/**
+ * Incremented every time a reactive change happens
+ * This is used to give computed a fast path to avoid re-compute when nothing
+ * has changed.
+ */
+export let globalVersion = 0
+
+/**
+ * @internal
+ */
+export class Dep {
+ version = 0
+ /**
+ * Link between this dep and the current active effect
+ */
+ activeLink?: Link = undefined
+ /**
+ * Doubly linked list representing the subscribing effects (tail)
+ */
+ subs?: Link = undefined
+
+ constructor(public computed?: ComputedRefImpl) {}
+
+ track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
+ if (!activeSub || !shouldTrack) {
+ return
+ }
+
+ let link = this.activeLink
+ if (link === undefined || link.sub !== activeSub) {
+ link = this.activeLink = {
+ dep: this,
+ sub: activeSub,
+ version: this.version,
+ nextDep: undefined,
+ prevDep: undefined,
+ nextSub: undefined,
+ prevSub: undefined,
+ prevActiveLink: undefined,
+ }
+
+ // add the link to the activeEffect as a dep (as tail)
+ if (!activeSub.deps) {
+ activeSub.deps = activeSub.depsTail = link
+ } else {
+ link.prevDep = activeSub.depsTail
+ activeSub.depsTail!.nextDep = link
+ activeSub.depsTail = link
+ }
+
+ if (activeSub.flags & EffectFlags.TRACKING) {
+ addSub(link)
+ }
+ } else if (link.version === -1) {
+ // reused from last run - already a sub, just sync version
+ link.version = this.version
+
+ // If this dep has a next, it means it's not at the tail - move it to the
+ // tail. This ensures the effect's dep list is in the order they are
+ // accessed during evaluation.
+ if (link.nextDep) {
+ const next = link.nextDep
+ next.prevDep = link.prevDep
+ if (link.prevDep) {
+ link.prevDep.nextDep = next
+ }
+
+ link.prevDep = activeSub.depsTail
+ link.nextDep = undefined
+ activeSub.depsTail!.nextDep = link
+ activeSub.depsTail = link
+
+ // this was the head - point to the new head
+ if (activeSub.deps === link) {
+ activeSub.deps = next
+ }
+ }
+ }
+
+ if (__DEV__ && activeSub.onTrack) {
+ activeSub.onTrack(
+ extend(
+ {
+ effect: activeSub,
+ },
+ debugInfo,
+ ),
+ )
+ }
+
+ return link
+ }
+
+ trigger(debugInfo?: DebuggerEventExtraInfo) {
+ this.version++
+ globalVersion++
+ this.notify(debugInfo)
+ }
+
+ notify(debugInfo?: DebuggerEventExtraInfo) {
+ startBatch()
+ try {
+ for (let link = this.subs; link; link = link.prevSub) {
+ if (
+ __DEV__ &&
+ link.sub.onTrigger &&
+ !(link.sub.flags & EffectFlags.NOTIFIED)
+ ) {
+ link.sub.onTrigger(
+ extend(
+ {
+ effect: link.sub,
+ },
+ debugInfo,
+ ),
+ )
+ }
+ link.sub.notify()
+ }
+ } finally {
+ endBatch()
+ }
+ }
+}
+
+function addSub(link: Link) {
+ const computed = link.dep.computed
+ // computed getting its first subscriber
+ // enable tracking + lazily subscribe to all its deps
+ if (computed && !link.dep.subs) {
+ computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
+ for (let l = computed.deps; l; l = l.nextDep) {
+ addSub(l)
+ }
+ }
+
+ const currentTail = link.dep.subs
+ if (currentTail !== link) {
+ link.prevSub = currentTail
+ if (currentTail) currentTail.nextSub = link
+ }
+ link.dep.subs = link
+}
+
+// The main WeakMap that stores {target -> key -> dep} connections.
+// Conceptually, it's easier to think of a dependency as a Dep class
+// which maintains a Set of subscribers, but we simply store them as
+// raw Maps to reduce memory overhead.
+type KeyToDepMap = Map<any, Dep>
+const targetMap = new WeakMap<object, KeyToDepMap>()
+
+export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
+export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map iterate' : '')
+
+/**
+ * Tracks access to a reactive property.
+ *
+ * This will check which effect is running at the moment and record it as dep
+ * which records all effects that depend on the reactive property.
+ *
+ * @param target - Object holding the reactive property.
+ * @param type - Defines the type of access to the reactive property.
+ * @param key - Identifier of the reactive property to track.
+ */
+export function track(target: object, type: TrackOpTypes, key: unknown) {
+ if (shouldTrack && activeSub) {
+ let depsMap = targetMap.get(target)
+ if (!depsMap) {
+ targetMap.set(target, (depsMap = new Map()))
+ }
+ let dep = depsMap.get(key)
+ if (!dep) {
+ depsMap.set(key, (dep = new Dep()))
+ }
+ if (__DEV__) {
+ dep.track({
+ target,
+ type,
+ key,
+ })
+ } else {
+ dep.track()
+ }
+ }
+}
+
+/**
+ * Finds all deps associated with the target (or a specific property) and
+ * triggers the effects stored within.
+ *
+ * @param target - The reactive object.
+ * @param type - Defines the type of the operation that needs to trigger effects.
+ * @param key - Can be used to target a specific reactive property in the target object.
+ */
+export function trigger(
+ target: object,
+ type: TriggerOpTypes,
+ key?: unknown,
+ newValue?: unknown,
+ oldValue?: unknown,
+ oldTarget?: Map<unknown, unknown> | Set<unknown>,
+) {
+ const depsMap = targetMap.get(target)
+ if (!depsMap) {
+ // never been tracked
+ globalVersion++
+ return
+ }
+
+ let deps: Dep[] = []
+ if (type === TriggerOpTypes.CLEAR) {
+ // collection being cleared
+ // trigger all effects for target
+ deps = [...depsMap.values()]
+ } else if (key === 'length' && isArray(target)) {
+ const newLength = Number(newValue)
+ depsMap.forEach((dep, key) => {
+ if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
+ deps.push(dep)
+ }
+ })
+ } else {
+ const push = (dep: Dep | undefined) => dep && deps.push(dep)
+
+ // schedule runs for SET | ADD | DELETE
+ if (key !== void 0) {
+ push(depsMap.get(key))
+ }
+
+ // also run for iteration key on ADD | DELETE | Map.SET
+ switch (type) {
+ case TriggerOpTypes.ADD:
+ if (!isArray(target)) {
+ push(depsMap.get(ITERATE_KEY))
+ if (isMap(target)) {
+ push(depsMap.get(MAP_KEY_ITERATE_KEY))
+ }
+ } else if (isIntegerKey(key)) {
+ // new index added to array -> length changes
+ push(depsMap.get('length'))
+ }
+ break
+ case TriggerOpTypes.DELETE:
+ if (!isArray(target)) {
+ push(depsMap.get(ITERATE_KEY))
+ if (isMap(target)) {
+ push(depsMap.get(MAP_KEY_ITERATE_KEY))
+ }
+ }
+ break
+ case TriggerOpTypes.SET:
+ if (isMap(target)) {
+ push(depsMap.get(ITERATE_KEY))
+ }
+ break
+ }
+ }
+
+ startBatch()
+ for (const dep of deps) {
+ if (__DEV__) {
+ dep.trigger({
+ target,
+ type,
+ key,
+ newValue,
+ oldValue,
+ oldTarget,
+ })
+ } else {
+ dep.trigger()
+ }
+ }
+ endBatch()
}
-export const createDep = (
- cleanup: () => void,
- computed?: ComputedRefImpl<any>,
-): Dep => {
- const dep = new Map() as Dep
- dep.cleanup = cleanup
- dep.computed = computed
- return dep
+/**
+ * Test only
+ */
+export function getDepFromReactive(object: any, key: string | number | symbol) {
+ return targetMap.get(object)?.get(key)
}
-import { NOOP, extend } from '@vue/shared'
+import { extend, hasChanged } from '@vue/shared'
import type { ComputedRefImpl } from './computed'
-import {
- DirtyLevels,
- type TrackOpTypes,
- type TriggerOpTypes,
-} from './constants'
-import type { Dep } from './dep'
-import { type EffectScope, recordEffectScope } from './effectScope'
+import type { TrackOpTypes, TriggerOpTypes } from './constants'
+import { type Dep, globalVersion } from './dep'
+import { recordEffectScope } from './effectScope'
+import { warn } from './warning'
export type EffectScheduler = (...args: any[]) => any
export type DebuggerEvent = {
- effect: ReactiveEffect
+ effect: Subscriber
} & DebuggerEventExtraInfo
export type DebuggerEventExtraInfo = {
oldTarget?: Map<any, any> | Set<any>
}
-export let activeEffect: ReactiveEffect | undefined
+export interface DebuggerOptions {
+ onTrack?: (event: DebuggerEvent) => void
+ onTrigger?: (event: DebuggerEvent) => void
+}
-export class ReactiveEffect<T = any> {
- active = true
- deps: Dep[] = []
+export interface ReactiveEffectOptions extends DebuggerOptions {
+ scheduler?: EffectScheduler
+ allowRecurse?: boolean
+ onStop?: () => void
+}
+export interface ReactiveEffectRunner<T = any> {
+ (): T
+ effect: ReactiveEffect
+}
+
+export let activeSub: Subscriber | undefined
+
+export enum EffectFlags {
+ ACTIVE = 1 << 0,
+ RUNNING = 1 << 1,
+ TRACKING = 1 << 2,
+ NOTIFIED = 1 << 3,
+ DIRTY = 1 << 4,
+ ALLOW_RECURSE = 1 << 5,
+ NO_BATCH = 1 << 6,
+}
+
+/**
+ * Subscriber is a type that tracks (or subscribes to) a list of deps.
+ */
+export interface Subscriber extends DebuggerOptions {
/**
- * Can be attached after creation
+ * Head of the doubly linked list representing the deps
* @internal
*/
- computed?: ComputedRefImpl<T>
+ deps?: Link
/**
+ * Tail of the same list
* @internal
*/
- allowRecurse?: boolean
+ depsTail?: Link
+ /**
+ * @internal
+ */
+ flags: EffectFlags
+ /**
+ * @internal
+ */
+ notify(): void
+}
- onStop?: () => void
- // dev only
- onTrack?: (event: DebuggerEvent) => void
- // dev only
- onTrigger?: (event: DebuggerEvent) => void
+/**
+ * Represents a link between a source (Dep) and a subscriber (Effect or Computed).
+ * Deps and subs have a many-to-many relationship - each link between a
+ * dep and a sub is represented by a Link instance.
+ *
+ * A Link is also a node in two doubly-linked lists - one for the associated
+ * sub to track all its deps, and one for the associated dep to track all its
+ * subs.
+ *
+ * @internal
+ */
+export interface Link {
+ dep: Dep
+ sub: Subscriber
+
+ /**
+ * - Before each effect run, all previous dep links' version are reset to -1
+ * - During the run, a link's version is synced with the source dep on access
+ * - After the run, links with version -1 (that were never used) are cleaned
+ * up
+ */
+ version: number
+ /**
+ * Pointers for doubly-linked lists
+ */
+ nextDep?: Link
+ prevDep?: Link
+
+ nextSub?: Link
+ prevSub?: Link
+
+ prevActiveLink?: Link
+}
+
+export class ReactiveEffect<T = any>
+ implements Subscriber, ReactiveEffectOptions
+{
/**
* @internal
*/
- _dirtyLevel = DirtyLevels.Dirty
+ deps?: Link = undefined
/**
* @internal
*/
- _trackId = 0
+ depsTail?: Link = undefined
/**
* @internal
*/
- _runnings = 0
+ flags: EffectFlags = EffectFlags.ACTIVE | EffectFlags.TRACKING
/**
* @internal
*/
- _shouldSchedule = false
+ nextEffect?: ReactiveEffect = undefined
/**
* @internal
*/
- _depsLength = 0
+ allowRecurse?: boolean
- constructor(
- public fn: () => T,
- public trigger: () => void,
- public scheduler?: EffectScheduler,
- scope?: EffectScope,
- ) {
- recordEffectScope(this, scope)
- }
+ scheduler?: EffectScheduler = undefined
+ onStop?: () => void
+ onTrack?: (event: DebuggerEvent) => void
+ onTrigger?: (event: DebuggerEvent) => void
- public get dirty() {
- if (
- this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect ||
- this._dirtyLevel === DirtyLevels.MaybeDirty
- ) {
- this._dirtyLevel = DirtyLevels.QueryingDirty
- pauseTracking()
- for (let i = 0; i < this._depsLength; i++) {
- const dep = this.deps[i]
- if (dep.computed) {
- triggerComputed(dep.computed)
- if (this._dirtyLevel >= DirtyLevels.Dirty) {
- break
- }
- }
- }
- if (this._dirtyLevel === DirtyLevels.QueryingDirty) {
- this._dirtyLevel = DirtyLevels.NotDirty
- }
- resetTracking()
- }
- return this._dirtyLevel >= DirtyLevels.Dirty
+ constructor(public fn: () => T) {
+ recordEffectScope(this)
}
- public set dirty(v) {
- this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty
+ /**
+ * @internal
+ */
+ notify() {
+ if (this.flags & EffectFlags.RUNNING && !this.allowRecurse) {
+ return
+ }
+ if (this.flags & EffectFlags.NO_BATCH) {
+ return this.trigger()
+ }
+ if (!(this.flags & EffectFlags.NOTIFIED)) {
+ this.flags |= EffectFlags.NOTIFIED
+ this.nextEffect = batchedEffect
+ batchedEffect = this
+ }
}
run() {
- this._dirtyLevel = DirtyLevels.NotDirty
- if (!this.active) {
+ // TODO cleanupEffect
+
+ if (!(this.flags & EffectFlags.ACTIVE)) {
+ // stopped during cleanup
return this.fn()
}
- let lastShouldTrack = shouldTrack
- let lastEffect = activeEffect
+
+ this.flags |= EffectFlags.RUNNING
+ prepareDeps(this)
+ const prevEffect = activeSub
+ const prevShouldTrack = shouldTrack
+ activeSub = this
+ shouldTrack = true
+
try {
- shouldTrack = true
- activeEffect = this
- this._runnings++
- preCleanupEffect(this)
return this.fn()
} finally {
- postCleanupEffect(this)
- this._runnings--
- activeEffect = lastEffect
- shouldTrack = lastShouldTrack
+ if (__DEV__ && activeSub !== this) {
+ warn(
+ 'Active effect was not restored correctly - ' +
+ 'this is likely a Vue internal bug.',
+ )
+ }
+ cleanupDeps(this)
+ activeSub = prevEffect
+ shouldTrack = prevShouldTrack
+ this.flags &= ~EffectFlags.RUNNING
}
}
stop() {
- if (this.active) {
- preCleanupEffect(this)
- postCleanupEffect(this)
- this.onStop?.()
- this.active = false
+ if (this.flags & EffectFlags.ACTIVE) {
+ for (let link = this.deps; link; link = link.nextDep) {
+ removeSub(link)
+ }
+ this.deps = this.depsTail = undefined
+ this.onStop && this.onStop()
+ this.flags &= ~EffectFlags.ACTIVE
+ }
+ }
+
+ trigger() {
+ if (this.scheduler) {
+ this.scheduler()
+ } else {
+ this.runIfDirty()
+ }
+ }
+
+ /**
+ * @internal
+ */
+ runIfDirty() {
+ if (isDirty(this)) {
+ this.run()
+ }
+ }
+
+ get dirty() {
+ return isDirty(this)
+ }
+}
+
+let batchDepth = 0
+let batchedEffect: ReactiveEffect | undefined
+
+/**
+ * @internal
+ */
+export function startBatch() {
+ batchDepth++
+}
+
+/**
+ * Run batched effects when all batches have ended
+ * @internal
+ */
+export function endBatch() {
+ if (batchDepth > 1) {
+ batchDepth--
+ return
+ }
+
+ let error: unknown
+ while (batchedEffect) {
+ let e: ReactiveEffect | undefined = batchedEffect
+ batchedEffect = undefined
+ while (e) {
+ const next: ReactiveEffect | undefined = e.nextEffect
+ e.nextEffect = undefined
+ e.flags &= ~EffectFlags.NOTIFIED
+ if (e.flags & EffectFlags.ACTIVE) {
+ try {
+ e.trigger()
+ } catch (err) {
+ if (!error) error = err
+ }
+ }
+ e = next
}
}
+
+ batchDepth--
+ if (error) throw error
}
-function triggerComputed(computed: ComputedRefImpl<any>) {
- return computed.value
+function prepareDeps(sub: Subscriber) {
+ // Prepare deps for tracking, starting from the head
+ for (let link = sub.deps; link; link = link.nextDep) {
+ // set all previous deps' (if any) version to -1 so that we can track
+ // which ones are unused after the run
+ link.version = -1
+ // store previous active sub if link was being used in another context
+ link.prevActiveLink = link.dep.activeLink
+ link.dep.activeLink = link
+ }
}
-function preCleanupEffect(effect: ReactiveEffect) {
- effect._trackId++
- effect._depsLength = 0
+function cleanupDeps(sub: Subscriber) {
+ // Cleanup unsued deps
+ let head
+ let tail = sub.depsTail
+ for (let link = tail; link; link = link.prevDep) {
+ if (link.version === -1) {
+ if (link === tail) tail = link.prevDep
+ // unused - remove it from the dep's subscribing effect list
+ removeSub(link)
+ // also remove it from this effect's dep list
+ removeDep(link)
+ } else {
+ // The new head is the last node seen which wasn't removed
+ // from the doubly-linked list
+ head = link
+ }
+
+ // restore previous active link if any
+ link.dep.activeLink = link.prevActiveLink
+ link.prevActiveLink = undefined
+ }
+ // set the new head & tail
+ sub.deps = head
+ sub.depsTail = tail
}
-function postCleanupEffect(effect: ReactiveEffect) {
- if (effect.deps.length > effect._depsLength) {
- for (let i = effect._depsLength; i < effect.deps.length; i++) {
- cleanupDepEffect(effect.deps[i], effect)
+function isDirty(sub: Subscriber): boolean {
+ for (let link = sub.deps; link; link = link.nextDep) {
+ if (
+ link.dep.version !== link.version ||
+ (link.dep.computed && refreshComputed(link.dep.computed) === false) ||
+ link.dep.version !== link.version
+ ) {
+ return true
}
- effect.deps.length = effect._depsLength
}
+ // @ts-expect-error only for backwards compatibility where libs manually set
+ // this flag - e.g. Pinia's testing module
+ if (sub._dirty) {
+ return true
+ }
+ return false
}
-function cleanupDepEffect(dep: Dep, effect: ReactiveEffect) {
- const trackId = dep.get(effect)
- if (trackId !== undefined && effect._trackId !== trackId) {
- dep.delete(effect)
- if (dep.size === 0) {
- dep.cleanup()
+/**
+ * Returning false indicates the refresh failed
+ * @internal
+ */
+export function refreshComputed(computed: ComputedRefImpl) {
+ if (computed.flags & EffectFlags.RUNNING) {
+ return false
+ }
+ if (
+ computed.flags & EffectFlags.TRACKING &&
+ !(computed.flags & EffectFlags.DIRTY)
+ ) {
+ return
+ }
+ computed.flags &= ~EffectFlags.DIRTY
+
+ // Global version fast path when no reactive changes has happened since
+ // last refresh.
+ if (computed.globalVersion === globalVersion) {
+ return
+ }
+ computed.globalVersion = globalVersion
+
+ const dep = computed.dep
+ computed.flags |= EffectFlags.RUNNING
+ // In SSR there will be no render effect, so the computed has no subscriber
+ // and therefore tracks no deps, thus we cannot rely on the dirty check.
+ // Instead, computed always re-evaluate and relies on the globalVersion
+ // fast path above for caching.
+ if (dep.version > 0 && !computed.isSSR && !isDirty(computed)) {
+ computed.flags &= ~EffectFlags.RUNNING
+ return
+ }
+
+ const prevSub = activeSub
+ const prevShouldTrack = shouldTrack
+ activeSub = computed
+ shouldTrack = true
+
+ try {
+ prepareDeps(computed)
+ const value = computed.fn()
+ if (dep.version === 0 || hasChanged(value, computed._value)) {
+ computed._value = value
+ dep.version++
}
+ } catch (err) {
+ dep.version++
}
+
+ activeSub = prevSub
+ shouldTrack = prevShouldTrack
+ cleanupDeps(computed)
+ computed.flags &= ~EffectFlags.RUNNING
}
-export interface DebuggerOptions {
- onTrack?: (event: DebuggerEvent) => void
- onTrigger?: (event: DebuggerEvent) => void
+function removeSub(link: Link) {
+ const { dep, prevSub, nextSub } = link
+ if (prevSub) {
+ prevSub.nextSub = nextSub
+ link.prevSub = undefined
+ }
+ if (nextSub) {
+ nextSub.prevSub = prevSub
+ link.nextSub = undefined
+ }
+ if (dep.subs === link) {
+ // was previous tail, point new tail to prev
+ dep.subs = prevSub
+ }
+
+ if (!dep.subs && dep.computed) {
+ // last subscriber removed
+ // if computed, unsubscribe it from all its deps so this computed and its
+ // value can be GCed
+ dep.computed.flags &= ~EffectFlags.TRACKING
+ for (let l = dep.computed.deps; l; l = l.nextDep) {
+ removeSub(l)
+ }
+ }
}
-export interface ReactiveEffectOptions extends DebuggerOptions {
- lazy?: boolean
- scheduler?: EffectScheduler
- scope?: EffectScope
- allowRecurse?: boolean
- onStop?: () => void
+function removeDep(link: Link) {
+ const { prevDep, nextDep } = link
+ if (prevDep) {
+ prevDep.nextDep = nextDep
+ link.prevDep = undefined
+ }
+ if (nextDep) {
+ nextDep.prevDep = prevDep
+ link.nextDep = undefined
+ }
}
export interface ReactiveEffectRunner<T = any> {
effect: ReactiveEffect
}
-/**
- * Registers the given function to track reactive updates.
- *
- * The given function will be run once immediately. Every time any reactive
- * property that's accessed within it gets updated, the function will run again.
- *
- * @param fn - The function that will track reactive updates.
- * @param options - Allows to control the effect's behaviour.
- * @returns A runner that can be used to control the effect after creation.
- */
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions,
-): ReactiveEffectRunner {
+): ReactiveEffectRunner<T> {
if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
- const _effect = new ReactiveEffect(fn, NOOP, () => {
- if (_effect.dirty) {
- _effect.run()
- }
- })
+ const e = new ReactiveEffect(fn)
if (options) {
- extend(_effect, options)
- if (options.scope) recordEffectScope(_effect, options.scope)
+ extend(e, options)
}
- if (!options || !options.lazy) {
- _effect.run()
+ try {
+ e.run()
+ } catch (err) {
+ e.stop()
+ throw err
}
- const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
- runner.effect = _effect
+ const runner = e.run.bind(e) as ReactiveEffectRunner
+ runner.effect = e
return runner
}
runner.effect.stop()
}
+/**
+ * @internal
+ */
export let shouldTrack = true
-export let pauseScheduleStack = 0
-
const trackStack: boolean[] = []
/**
const last = trackStack.pop()
shouldTrack = last === undefined ? true : last
}
-
-export function pauseScheduling() {
- pauseScheduleStack++
-}
-
-export function resetScheduling() {
- pauseScheduleStack--
- while (!pauseScheduleStack && queueEffectSchedulers.length) {
- queueEffectSchedulers.shift()!()
- }
-}
-
-export function trackEffect(
- effect: ReactiveEffect,
- dep: Dep,
- debuggerEventExtraInfo?: DebuggerEventExtraInfo,
-) {
- if (dep.get(effect) !== effect._trackId) {
- dep.set(effect, effect._trackId)
- const oldDep = effect.deps[effect._depsLength]
- if (oldDep !== dep) {
- if (oldDep) {
- cleanupDepEffect(oldDep, effect)
- }
- effect.deps[effect._depsLength++] = dep
- } else {
- effect._depsLength++
- }
- if (__DEV__) {
- effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!))
- }
- }
-}
-
-const queueEffectSchedulers: EffectScheduler[] = []
-
-export function triggerEffects(
- dep: Dep,
- dirtyLevel: DirtyLevels,
- debuggerEventExtraInfo?: DebuggerEventExtraInfo,
-) {
- pauseScheduling()
- for (const effect of dep.keys()) {
- // dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result
- let tracking: boolean | undefined
- if (
- effect._dirtyLevel < dirtyLevel &&
- (tracking ??= dep.get(effect) === effect._trackId)
- ) {
- effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty
- effect._dirtyLevel = dirtyLevel
- }
- if (
- effect._shouldSchedule &&
- (tracking ??= dep.get(effect) === effect._trackId)
- ) {
- if (__DEV__) {
- effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
- }
- effect.trigger()
- if (
- (!effect._runnings || effect.allowRecurse) &&
- effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect
- ) {
- effect._shouldSchedule = false
- if (effect.scheduler) {
- queueEffectSchedulers.push(effect.scheduler)
- }
- }
- }
- }
- resetScheduling()
-}
type ComputedGetter,
type ComputedSetter,
} from './computed'
-export { deferredComputed } from './deferredComputed'
export {
effect,
stop,
enableTracking,
pauseTracking,
resetTracking,
- pauseScheduling,
- resetScheduling,
ReactiveEffect,
+ EffectFlags,
type ReactiveEffectRunner,
type ReactiveEffectOptions,
type EffectScheduler,
type DebuggerEvent,
type DebuggerEventExtraInfo,
} from './effect'
-export { trigger, track, ITERATE_KEY } from './reactiveEffect'
+export { trigger, track, ITERATE_KEY } from './dep'
export {
effectScope,
EffectScope,
+++ /dev/null
-import { isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
-import { DirtyLevels, type TrackOpTypes, TriggerOpTypes } from './constants'
-import { type Dep, createDep } from './dep'
-import {
- activeEffect,
- pauseScheduling,
- resetScheduling,
- shouldTrack,
- trackEffect,
- triggerEffects,
-} from './effect'
-
-// The main WeakMap that stores {target -> key -> dep} connections.
-// Conceptually, it's easier to think of a dependency as a Dep class
-// which maintains a Set of subscribers, but we simply store them as
-// raw Maps to reduce memory overhead.
-type KeyToDepMap = Map<any, Dep>
-const targetMap = new WeakMap<object, KeyToDepMap>()
-
-export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
-export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '')
-
-/**
- * Tracks access to a reactive property.
- *
- * This will check which effect is running at the moment and record it as dep
- * which records all effects that depend on the reactive property.
- *
- * @param target - Object holding the reactive property.
- * @param type - Defines the type of access to the reactive property.
- * @param key - Identifier of the reactive property to track.
- */
-export function track(target: object, type: TrackOpTypes, key: unknown) {
- if (shouldTrack && activeEffect) {
- let depsMap = targetMap.get(target)
- if (!depsMap) {
- targetMap.set(target, (depsMap = new Map()))
- }
- let dep = depsMap.get(key)
- if (!dep) {
- depsMap.set(key, (dep = createDep(() => depsMap!.delete(key))))
- }
- trackEffect(
- activeEffect,
- dep,
- __DEV__
- ? {
- target,
- type,
- key,
- }
- : void 0,
- )
- }
-}
-
-/**
- * Finds all deps associated with the target (or a specific property) and
- * triggers the effects stored within.
- *
- * @param target - The reactive object.
- * @param type - Defines the type of the operation that needs to trigger effects.
- * @param key - Can be used to target a specific reactive property in the target object.
- */
-export function trigger(
- target: object,
- type: TriggerOpTypes,
- key?: unknown,
- newValue?: unknown,
- oldValue?: unknown,
- oldTarget?: Map<unknown, unknown> | Set<unknown>,
-) {
- const depsMap = targetMap.get(target)
- if (!depsMap) {
- // never been tracked
- return
- }
-
- let deps: (Dep | undefined)[] = []
- if (type === TriggerOpTypes.CLEAR) {
- // collection being cleared
- // trigger all effects for target
- deps = [...depsMap.values()]
- } else if (key === 'length' && isArray(target)) {
- const newLength = Number(newValue)
- depsMap.forEach((dep, key) => {
- if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
- deps.push(dep)
- }
- })
- } else {
- // schedule runs for SET | ADD | DELETE
- if (key !== void 0) {
- deps.push(depsMap.get(key))
- }
-
- // also run for iteration key on ADD | DELETE | Map.SET
- switch (type) {
- case TriggerOpTypes.ADD:
- if (!isArray(target)) {
- deps.push(depsMap.get(ITERATE_KEY))
- if (isMap(target)) {
- deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
- }
- } else if (isIntegerKey(key)) {
- // new index added to array -> length changes
- deps.push(depsMap.get('length'))
- }
- break
- case TriggerOpTypes.DELETE:
- if (!isArray(target)) {
- deps.push(depsMap.get(ITERATE_KEY))
- if (isMap(target)) {
- deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
- }
- }
- break
- case TriggerOpTypes.SET:
- if (isMap(target)) {
- deps.push(depsMap.get(ITERATE_KEY))
- }
- break
- }
- }
-
- pauseScheduling()
- for (const dep of deps) {
- if (dep) {
- triggerEffects(
- dep,
- DirtyLevels.Dirty,
- __DEV__
- ? {
- target,
- type,
- key,
- newValue,
- oldValue,
- oldTarget,
- }
- : void 0,
- )
- }
- }
- resetScheduling()
-}
-
-export function getDepFromReactive(object: any, key: string | number | symbol) {
- return targetMap.get(object)?.get(key)
-}
-import type { ComputedRef } from './computed'
-import {
- activeEffect,
- shouldTrack,
- trackEffect,
- triggerEffects,
-} from './effect'
-import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants'
import {
type IfAny,
hasChanged,
isFunction,
isObject,
} from '@vue/shared'
+import { Dep, getDepFromReactive } from './dep'
import {
+ type ShallowReactiveMarker,
isProxy,
isReactive,
isReadonly,
toRaw,
toReactive,
} from './reactive'
-import type { ShallowReactiveMarker } from './reactive'
-import { type Dep, createDep } from './dep'
-import { ComputedRefImpl } from './computed'
-import { getDepFromReactive } from './reactiveEffect'
+import type { ComputedRef } from './computed'
+import { TrackOpTypes, TriggerOpTypes } from './constants'
declare const RefSymbol: unique symbol
export declare const RawSymbol: unique symbol
[RefSymbol]: true
}
-type RefBase<T> = {
- dep?: Dep
- value: T
-}
-
-export function trackRefValue(ref: RefBase<any>) {
- if (shouldTrack && activeEffect) {
- ref = toRaw(ref)
- trackEffect(
- activeEffect,
- (ref.dep ??= createDep(
- () => (ref.dep = undefined),
- ref instanceof ComputedRefImpl ? ref : undefined,
- )),
- __DEV__
- ? {
- target: ref,
- type: TrackOpTypes.GET,
- key: 'value',
- }
- : void 0,
- )
- }
-}
-
-export function triggerRefValue(
- ref: RefBase<any>,
- dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
- newVal?: any,
-) {
- ref = toRaw(ref)
- const dep = ref.dep
- if (dep) {
- triggerEffects(
- dep,
- dirtyLevel,
- __DEV__
- ? {
- target: ref,
- type: TriggerOpTypes.SET,
- key: 'value',
- newValue: newVal,
- }
- : void 0,
- )
- }
-}
-
/**
* Checks if a value is a ref object.
*
*/
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref {
- return !!(r && r.__v_isRef === true)
+ return r ? r.__v_isRef === true : false
}
/**
return new RefImpl(rawValue, shallow)
}
-class RefImpl<T> {
- private _value: T
+/**
+ * @internal
+ */
+class RefImpl<T = any> {
+ _value: T
private _rawValue: T
- public dep?: Dep = undefined
+ dep: Dep = new Dep()
+
public readonly __v_isRef = true
constructor(
}
get value() {
- trackRefValue(this)
+ if (__DEV__) {
+ this.dep.track({
+ target: this,
+ type: TrackOpTypes.GET,
+ key: 'value',
+ })
+ } else {
+ this.dep.track()
+ }
return this._value
}
- set value(newVal) {
+ set value(newValue) {
+ const oldValue = this._rawValue
const useDirectValue =
- this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
- newVal = useDirectValue ? newVal : toRaw(newVal)
- if (hasChanged(newVal, this._rawValue)) {
- this._rawValue = newVal
- this._value = useDirectValue ? newVal : toReactive(newVal)
- triggerRefValue(this, DirtyLevels.Dirty, newVal)
+ this.__v_isShallow || isShallow(newValue) || isReadonly(newValue)
+ newValue = useDirectValue ? newValue : toRaw(newValue)
+ if (hasChanged(newValue, oldValue)) {
+ this._rawValue = newValue
+ this._value = useDirectValue ? newValue : toReactive(newValue)
+ if (__DEV__) {
+ this.dep.trigger({
+ target: this,
+ type: TriggerOpTypes.SET,
+ key: 'value',
+ newValue,
+ oldValue,
+ })
+ } else {
+ this.dep.trigger()
+ }
}
}
}
* @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref}
*/
export function triggerRef(ref: Ref) {
- triggerRefValue(ref, DirtyLevels.Dirty, __DEV__ ? ref.value : void 0)
+ if (__DEV__) {
+ ;(ref as unknown as RefImpl).dep.trigger({
+ target: ref,
+ type: TriggerOpTypes.SET,
+ key: 'value',
+ newValue: (ref as unknown as RefImpl)._value,
+ })
+ } else {
+ ;(ref as unknown as RefImpl).dep.trigger()
+ }
}
export type MaybeRef<T = any> = T | Ref<T>
}
class CustomRefImpl<T> {
- public dep?: Dep = undefined
+ public dep: Dep
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
public readonly __v_isRef = true
constructor(factory: CustomRefFactory<T>) {
- const { get, set } = factory(
- () => trackRefValue(this),
- () => triggerRefValue(this),
- )
+ const dep = (this.dep = new Dep())
+ const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep))
this._get = get
this._set = set
}
import {
type ComponentInternalInstance,
- type ComputedRef,
type SetupContext,
Suspense,
computed,
withAsyncContext,
withDefaults,
} from '../src/apiSetupHelpers'
+import type { ComputedRefImpl } from '../../reactivity/src/computed'
+import { EffectFlags, type ReactiveEffectRunner, effect } from '@vue/reactivity'
describe('SFC <script setup> helpers', () => {
test('should warn runtime usage', () => {
resolve = r
})
- let c: ComputedRef
+ let c: ComputedRefImpl
+ let e: ReactiveEffectRunner
const Comp = defineComponent({
async setup() {
__temp = await __temp
__restore()
- c = computed(() => {})
+ c = computed(() => {}) as unknown as ComputedRefImpl
+ e = effect(() => c.value)
// register the lifecycle after an await statement
onMounted(resolve)
- return () => ''
+ return () => c.value
},
})
app.mount(root)
await ready
- expect(c!.effect.active).toBe(true)
+ expect(e!.effect.flags & EffectFlags.ACTIVE).toBeTruthy()
+ expect(c!.flags & EffectFlags.TRACKING).toBeTruthy()
app.unmount()
- expect(c!.effect.active).toBe(false)
+ expect(e!.effect.flags & EffectFlags.ACTIVE).toBeFalsy()
+ expect(c!.flags & EffectFlags.TRACKING).toBeFalsy()
})
})
})
} from '@vue/runtime-test'
import {
type DebuggerEvent,
+ EffectFlags,
ITERATE_KEY,
type Ref,
type ShallowRef,
await nextTick()
await nextTick()
- expect(instance!.scope.effects[0].active).toBe(false)
+ expect(instance!.scope.effects[0].flags & EffectFlags.ACTIVE).toBeFalsy()
})
test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
unwatch!()
expect(scope.effects.length).toBe(0)
})
+
+ // simplified case of VueUse syncRef
+ test('sync watcher should not be batched', () => {
+ const a = ref(0)
+ const b = ref(0)
+ let pauseB = false
+ watch(
+ a,
+ () => {
+ pauseB = true
+ b.value = a.value + 1
+ pauseB = false
+ },
+ { flush: 'sync' },
+ )
+ watch(
+ b,
+ () => {
+ if (!pauseB) {
+ throw new Error('should not be called')
+ }
+ },
+ { flush: 'sync' },
+ )
+
+ a.value = 1
+ expect(b.value).toBe(2)
+ })
+
+ test('watchEffect should not fire on computed deps that did not change', async () => {
+ const a = ref(0)
+ const c = computed(() => a.value % 2)
+ const spy = vi.fn()
+ watchEffect(() => {
+ spy()
+ c.value
+ })
+ expect(spy).toHaveBeenCalledTimes(1)
+ a.value += 2
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(1)
+ })
})
if (instance.parent && isKeepAlive(instance.parent.vnode)) {
// parent is keep-alive, force update so the loaded component's
// name is taken into account
- instance.parent.effect.dirty = true
queueJob(instance.parent.update)
}
})
import {
type ComputedRef,
type DebuggerOptions,
+ EffectFlags,
type EffectScheduler,
ReactiveEffect,
ReactiveFlags,
let oldValue: any = isMultiSource
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
: INITIAL_WATCHER_VALUE
- const job: SchedulerJob = () => {
- if (!effect.active || !effect.dirty) {
+ const job: SchedulerJob = (immediateFirstRun?: boolean) => {
+ if (
+ !(effect.flags & EffectFlags.ACTIVE) ||
+ (!effect.dirty && !immediateFirstRun)
+ ) {
return
}
if (cb) {
// it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb
+ const effect = new ReactiveEffect(getter)
+
let scheduler: EffectScheduler
if (flush === 'sync') {
+ effect.flags |= EffectFlags.NO_BATCH
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
if (instance) job.id = instance.uid
scheduler = () => queueJob(job)
}
-
- const effect = new ReactiveEffect(getter, NOOP, scheduler)
+ effect.scheduler = scheduler
const scope = getCurrentScope()
const unwatch = () => {
// initial run
if (cb) {
if (immediate) {
- job()
+ job(true)
} else {
oldValue = effect.run()
}
*/
effect: ReactiveEffect
/**
- * Bound effect runner to be passed to schedulers
+ * Force update render effect
*/
- update: SchedulerJob
+ update: () => void
+ /**
+ * Render effect job to be passed to scheduler (checks if dirty)
+ */
+ job: SchedulerJob
/**
* The render function that returns vdom tree.
* @internal
subTree: null!, // will be set synchronously right after creation
effect: null!,
update: null!, // will be set synchronously right after creation
+ job: null!,
scope: new EffectScope(true /* detached */),
render: null,
proxy: null,
$forceUpdate: i =>
i.f ||
(i.f = () => {
- i.effect.dirty = true
queueJob(i.update)
}),
$nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),
state.isLeaving = false
// #6835
// it also needs to be updated when active is undefined
- if (instance.update.active !== false) {
- instance.effect.dirty = true
+ if (instance.job.active !== false) {
instance.update()
}
}
{},
['span', vueStyle, genRefFlag(obj)],
'<',
- formatValue(obj.value),
+ // avoid debugger accessing value affecting behavior
+ formatValue('_value' in obj ? obj._value : obj),
`>`,
]
} else if (isReactive(obj)) {
instance.renderCache = []
// this flag forces child components with slot content to update
isHmrUpdating = true
- instance.effect.dirty = true
instance.update()
isHmrUpdating = false
})
// 4. Force the parent instance to re-render. This will cause all updated
// components to be unmounted and re-mounted. Queue the update so that we
// don't end up forcing the same parent to re-render multiple times.
- instance.parent.effect.dirty = true
queueJob(instance.parent.update)
} else if (instance.appContext.reload) {
// root instance mounted via createApp() has a reload method
// double updating the same child component in the same flush.
invalidateJob(instance.update)
// instance.update is the reactive effect.
- instance.effect.dirty = true
instance.update()
}
} else {
}
// create reactive effect for rendering
- const effect = (instance.effect = new ReactiveEffect(
- componentUpdateFn,
- NOOP,
- () => queueJob(update),
- instance.scope, // track it in component's effect scope
- ))
-
- const update: SchedulerJob = (instance.update = () => {
- if (effect.dirty) {
- effect.run()
- }
- })
- update.id = instance.uid
+ instance.scope.on()
+ const effect = (instance.effect = new ReactiveEffect(componentUpdateFn))
+ instance.scope.off()
+
+ const update = (instance.update = effect.run.bind(effect))
+ const job: SchedulerJob = (instance.job = effect.runIfDirty.bind(effect))
+ job.id = instance.uid
+ effect.scheduler = () => queueJob(job)
+
// allowRecurse
// #1801, #2043 component render effects should allow recursive updates
toggleRecurse(instance, true)
effect.onTrigger = instance.rtg
? e => invokeArrayFns(instance.rtg!, e)
: void 0
- update.ownerInstance = instance
+ job.ownerInstance = instance
}
update()
unregisterHMR(instance)
}
- const { bum, scope, update, subTree, um } = instance
+ const { bum, scope, job, subTree, um } = instance
// beforeUnmount hook
if (bum) {
// stop effects in component scope
scope.stop()
- // update may be null if a component is unmounted before its async
+ // job may be null if a component is unmounted before its async
// setup has resolved.
- if (update) {
+ if (job) {
// so that scheduler will no longer invoke it
- update.active = false
+ job.active = false
unmount(subTree, instance, parentSuspense, doRemove)
}
// unmounted hook
}
function toggleRecurse(
- { effect, update }: ComponentInternalInstance,
+ { effect, job }: ComponentInternalInstance,
allowed: boolean,
) {
- effect.allowRecurse = update.allowRecurse = allowed
+ effect.allowRecurse = job.allowRecurse = allowed
}
export function needTransition(
id?: number
pre?: boolean
active?: boolean
- computed?: boolean
/**
* Indicates whether the effect is allowed to recursively trigger itself
* when managed by the scheduler.
-import { computed, createSSRApp, defineComponent, h, reactive } from 'vue'
+import { computed, createSSRApp, defineComponent, h, reactive, ref } from 'vue'
import { renderToString } from '../src/renderToString'
// #5208 reported memory leak of keeping computed alive during SSR
// during the render phase
expect(getterSpy).toHaveBeenCalledTimes(2)
})
+
+// although we technically shouldn't allow state mutation during render,
+// it does sometimes happen
+test('computed mutation during render', async () => {
+ const App = defineComponent(async () => {
+ const n = ref(0)
+ const m = computed(() => n.value + 1)
+
+ m.value // force non-dirty
+
+ return () => {
+ n.value++
+ return h('div', null, `value: ${m.value}`)
+ }
+ })
+
+ const app = createSSRApp(App)
+ const html = await renderToString(app)
+ expect(html).toMatch('value: 2')
+})
comp.ssrRender = ssrCompile(comp.template, instance)
}
- // perf: enable caching of computed getters during render
- // since there cannot be state mutations during render.
- for (const e of instance.scope.effects) {
- if (e.computed) {
- e.computed._dirty = true
- e.computed._cacheable = true
- }
- }
-
const ssrRender = instance.ssrRender || comp.ssrRender
if (ssrRender) {
// optimized