Refactor reactivity system to use version counting and doubly-linked list tracking (#10397)

Bug fixes
close #10236
close #10069

PRs made stale by this one
close #10290
close #10354
close #10189
close #9480
This commit is contained in:
Evan You 2024-02-25 16:51:49 +08:00 committed by GitHub
parent 272ab9fbdc
commit 05eb4e0fef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 1677 additions and 1170 deletions

View File

@ -0,0 +1,200 @@
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
})
}
})

View File

@ -0,0 +1,111 @@
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)
})

View File

@ -3,7 +3,7 @@ import { computed, reactive, readonly, shallowRef, triggerRef } from '../src'
for (let amount = 1e1; amount < 1e4; amount *= 10) { for (let amount = 1e1; amount < 1e4; amount *= 10) {
{ {
const rawArray = [] const rawArray: any[] = []
for (let i = 0, n = amount; i < n; i++) { for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i) rawArray.push(i)
} }
@ -21,7 +21,7 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) {
} }
{ {
const rawArray = [] const rawArray: any[] = []
for (let i = 0, n = amount; i < n; i++) { for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i) rawArray.push(i)
} }
@ -40,7 +40,7 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) {
} }
{ {
const rawArray = [] const rawArray: any[] = []
for (let i = 0, n = amount; i < n; i++) { for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i) rawArray.push(i)
} }
@ -56,7 +56,7 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) {
} }
{ {
const rawArray = [] const rawArray: any[] = []
for (let i = 0, n = amount; i < n; i++) { for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i) rawArray.push(i)
} }

View File

@ -79,7 +79,7 @@ bench('create reactive map', () => {
{ {
const r = reactive(createMap({ a: 1 })) const r = reactive(createMap({ a: 1 }))
const computeds = [] const computeds: any[] = []
for (let i = 0, n = 1000; i < n; i++) { for (let i = 0, n = 1000; i < n; i++) {
const c = computed(() => { const c = computed(() => {
return r.get('a') * 2 return r.get('a') * 2
@ -94,7 +94,7 @@ bench('create reactive map', () => {
{ {
const r = reactive(createMap({ a: 1 })) const r = reactive(createMap({ a: 1 }))
const computeds = [] const computeds: any[] = []
for (let i = 0, n = 1000; i < n; i++) { for (let i = 0, n = 1000; i < n; i++) {
const c = computed(() => { const c = computed(() => {
return r.get('a') * 2 return r.get('a') * 2

View File

@ -0,0 +1,21 @@
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++
})
}

View File

@ -26,7 +26,6 @@ describe('ref', () => {
const v = ref(100) const v = ref(100)
bench('write/read ref', () => { bench('write/read ref', () => {
v.value = i++ v.value = i++
v.value v.value
}) })
} }

View File

@ -1,126 +0,0 @@
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
})
}
})

View File

@ -1,4 +1,12 @@
import { h, nextTick, nodeOps, render, serializeInner } from '@vue/runtime-test' import {
h,
nextTick,
nodeOps,
onMounted,
onUnmounted,
render,
serializeInner,
} from '@vue/runtime-test'
import { import {
type DebuggerEvent, type DebuggerEvent,
ITERATE_KEY, ITERATE_KEY,
@ -13,8 +21,8 @@ import {
shallowRef, shallowRef,
toRaw, toRaw,
} from '../src' } from '../src'
import { DirtyLevels } from '../src/constants' import { EffectFlags, pauseTracking, resetTracking } from '../src/effect'
import { COMPUTED_SIDE_EFFECT_WARN } from '../src/computed' import type { ComputedRef, ComputedRefImpl } from '../src/computed'
describe('reactivity/computed', () => { describe('reactivity/computed', () => {
it('should return updated value', () => { it('should return updated value', () => {
@ -123,21 +131,6 @@ describe('reactivity/computed', () => {
expect(getter2).toHaveBeenCalledTimes(2) 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', () => { it('should support setter', () => {
const n = ref(1) const n = ref(1)
const plusOne = computed({ const plusOne = computed({
@ -219,12 +212,6 @@ describe('reactivity/computed', () => {
expect(isReadonly(z.value.a)).toBe(false) 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', () => { it('debug: onTrack', () => {
let events: DebuggerEvent[] = [] let events: DebuggerEvent[] = []
const onTrack = vi.fn((e: DebuggerEvent) => { const onTrack = vi.fn((e: DebuggerEvent) => {
@ -238,19 +225,19 @@ describe('reactivity/computed', () => {
expect(onTrack).toHaveBeenCalledTimes(3) expect(onTrack).toHaveBeenCalledTimes(3)
expect(events).toEqual([ expect(events).toEqual([
{ {
effect: c.effect, effect: c,
target: toRaw(obj), target: toRaw(obj),
type: TrackOpTypes.GET, type: TrackOpTypes.GET,
key: 'foo', key: 'foo',
}, },
{ {
effect: c.effect, effect: c,
target: toRaw(obj), target: toRaw(obj),
type: TrackOpTypes.HAS, type: TrackOpTypes.HAS,
key: 'bar', key: 'bar',
}, },
{ {
effect: c.effect, effect: c,
target: toRaw(obj), target: toRaw(obj),
type: TrackOpTypes.ITERATE, type: TrackOpTypes.ITERATE,
key: ITERATE_KEY, key: ITERATE_KEY,
@ -266,14 +253,14 @@ describe('reactivity/computed', () => {
const obj = reactive<{ foo?: number }>({ foo: 1 }) const obj = reactive<{ foo?: number }>({ foo: 1 })
const c = computed(() => obj.foo, { onTrigger }) const c = computed(() => obj.foo, { onTrigger })
// computed won't trigger compute until accessed // computed won't track until it has a subscriber
c.value effect(() => c.value)
obj.foo!++ obj.foo!++
expect(c.value).toBe(2) expect(c.value).toBe(2)
expect(onTrigger).toHaveBeenCalledTimes(1) expect(onTrigger).toHaveBeenCalledTimes(1)
expect(events[0]).toEqual({ expect(events[0]).toEqual({
effect: c.effect, effect: c,
target: toRaw(obj), target: toRaw(obj),
type: TriggerOpTypes.SET, type: TriggerOpTypes.SET,
key: 'foo', key: 'foo',
@ -285,7 +272,7 @@ describe('reactivity/computed', () => {
expect(c.value).toBeUndefined() expect(c.value).toBeUndefined()
expect(onTrigger).toHaveBeenCalledTimes(2) expect(onTrigger).toHaveBeenCalledTimes(2)
expect(events[1]).toEqual({ expect(events[1]).toEqual({
effect: c.effect, effect: c,
target: toRaw(obj), target: toRaw(obj),
type: TriggerOpTypes.DELETE, type: TriggerOpTypes.DELETE,
key: 'foo', key: 'foo',
@ -380,17 +367,17 @@ describe('reactivity/computed', () => {
const a = ref(0) const a = ref(0)
const b = computed(() => { const b = computed(() => {
return a.value % 3 !== 0 return a.value % 3 !== 0
}) }) as unknown as ComputedRefImpl
const c = computed(() => { const c = computed(() => {
cSpy() cSpy()
if (a.value % 3 === 2) { if (a.value % 3 === 2) {
return 'expensive' return 'expensive'
} }
return 'cheap' return 'cheap'
}) }) as unknown as ComputedRefImpl
const d = computed(() => { const d = computed(() => {
return a.value % 3 === 2 return a.value % 3 === 2
}) }) as unknown as ComputedRefImpl
const e = computed(() => { const e = computed(() => {
if (b.value) { if (b.value) {
if (d.value) { if (d.value) {
@ -398,16 +385,15 @@ describe('reactivity/computed', () => {
} }
} }
return c.value return c.value
}) }) as unknown as ComputedRefImpl
e.value e.value
a.value++ a.value++
e.value e.value
expect(e.effect.deps.length).toBe(3) expect(e.deps!.dep).toBe(b.dep)
expect(e.effect.deps.indexOf((b as any).dep)).toBe(0) expect(e.deps!.nextDep!.dep).toBe(d.dep)
expect(e.effect.deps.indexOf((d as any).dep)).toBe(1) expect(e.deps!.nextDep!.nextDep!.dep).toBe(c.dep)
expect(e.effect.deps.indexOf((c as any).dep)).toBe(2)
expect(cSpy).toHaveBeenCalledTimes(2) expect(cSpy).toHaveBeenCalledTimes(2)
a.value++ a.value++
@ -456,17 +442,14 @@ describe('reactivity/computed', () => {
expect(fnSpy).toBeCalledTimes(2) 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 v = ref(1)
const c1 = computed(() => v.value) const c1 = computed(() => v.value) as unknown as ComputedRefImpl
const c2 = computed(() => c1.value) const c2 = computed(() => c1.value) as unknown as ComputedRefImpl
c1.effect.allowRecurse = true
c2.effect.allowRecurse = true
c2.value c2.value
expect(c1.flags & EffectFlags.DIRTY).toBeFalsy()
expect(c1.effect._dirtyLevel).toBe(DirtyLevels.NotDirty) expect(c2.flags & EffectFlags.DIRTY).toBeFalsy()
expect(c2.effect._dirtyLevel).toBe(DirtyLevels.NotDirty)
}) })
it('should chained computeds dirtyLevel update with first computed effect', () => { it('should chained computeds dirtyLevel update with first computed effect', () => {
@ -481,15 +464,7 @@ describe('reactivity/computed', () => {
const c3 = computed(() => c2.value) const c3 = computed(() => c2.value)
c3.value c3.value
// expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
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()
}) })
it('should work when chained(ref+computed)', () => { it('should work when chained(ref+computed)', () => {
@ -502,9 +477,8 @@ describe('reactivity/computed', () => {
}) })
const c2 = computed(() => v.value + c1.value) const c2 = computed(() => v.value + c1.value)
expect(c2.value).toBe('0foo') expect(c2.value).toBe('0foo')
expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
expect(c2.value).toBe('1foo') 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', () => { it('should trigger effect even computed already dirty', () => {
@ -519,15 +493,16 @@ describe('reactivity/computed', () => {
const c2 = computed(() => v.value + c1.value) const c2 = computed(() => v.value + c1.value)
effect(() => { effect(() => {
fnSpy() fnSpy(c2.value)
c2.value
}) })
expect(fnSpy).toBeCalledTimes(1) expect(fnSpy).toBeCalledTimes(1)
expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty) expect(fnSpy.mock.calls).toMatchObject([['0foo']])
expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty) expect(v.value).toBe(1)
v.value = 2 v.value = 2
expect(fnSpy).toBeCalledTimes(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 // #10185
@ -553,25 +528,12 @@ describe('reactivity/computed', () => {
c3.value c3.value
v2.value = true v2.value = true
expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
c3.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,
)
v1.value.v.value = 999 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(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 () => { it('should be not dirty after deps mutate (mutate deps in computed)', async () => {
@ -593,10 +555,10 @@ describe('reactivity/computed', () => {
await nextTick() await nextTick()
await nextTick() await nextTick()
expect(serializeInner(root)).toBe(`2`) 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 v = ref('Hello')
const c = computed(() => { const c = computed(() => {
v.value += ' World' v.value += ' World'
@ -615,7 +577,279 @@ describe('reactivity/computed', () => {
v.value += ' World' v.value += ' World'
await nextTick() await nextTick()
expect(serializeInner(root)).toBe('Hello World World World World') 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'])
}) })
}) })

View File

@ -1,155 +0,0 @@
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)
})
})

View File

@ -11,8 +11,7 @@ import {
stop, stop,
toRaw, toRaw,
} from '../src/index' } from '../src/index'
import { pauseScheduling, resetScheduling } from '../src/effect' import { type Dep, ITERATE_KEY, getDepFromReactive } from '../src/dep'
import { ITERATE_KEY, getDepFromReactive } from '../src/reactiveEffect'
import { import {
computed, computed,
h, h,
@ -22,6 +21,12 @@ import {
render, render,
serializeInner, serializeInner,
} from '@vue/runtime-test' } from '@vue/runtime-test'
import {
endBatch,
pauseTracking,
resetTracking,
startBatch,
} from '../src/effect'
describe('reactivity/effect', () => { describe('reactivity/effect', () => {
it('should run the passed function once (wrapped by a effect)', () => { it('should run the passed function once (wrapped by a effect)', () => {
@ -698,18 +703,6 @@ describe('reactivity/effect', () => {
expect(dummy).toBe(1) 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', () => { it('scheduler', () => {
let dummy let dummy
let run: any let run: any
@ -1005,7 +998,7 @@ describe('reactivity/effect', () => {
}) })
}) })
it('should be triggered once with pauseScheduling', () => { it('should be triggered once with batching', () => {
const counter = reactive({ num: 0 }) const counter = reactive({ num: 0 })
const counterSpy = vi.fn(() => counter.num) const counterSpy = vi.fn(() => counter.num)
@ -1013,10 +1006,10 @@ describe('reactivity/effect', () => {
counterSpy.mockClear() counterSpy.mockClear()
pauseScheduling() startBatch()
counter.num++ counter.num++
counter.num++ counter.num++
resetScheduling() endBatch()
expect(counterSpy).toHaveBeenCalledTimes(1) expect(counterSpy).toHaveBeenCalledTimes(1)
}) })
@ -1049,47 +1042,76 @@ describe('reactivity/effect', () => {
expect(renderSpy).toHaveBeenCalledTimes(2) 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', () => { it('should remove the dep when the effect is stopped', () => {
const obj = reactive({ prop: 1 }) const obj = reactive({ prop: 1 })
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
const runner = effect(() => obj.prop) const runner = effect(() => obj.prop)
const dep = getDepFromReactive(toRaw(obj), 'prop') const dep = getDepFromReactive(toRaw(obj), 'prop')
expect(dep).toHaveLength(1) expect(getSubCount(dep)).toBe(1)
obj.prop = 2 obj.prop = 2
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep) expect(getSubCount(dep)).toBe(1)
expect(dep).toHaveLength(1)
stop(runner) stop(runner)
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined() expect(getSubCount(dep)).toBe(0)
obj.prop = 3 obj.prop = 3
runner() runner()
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined() expect(getSubCount(dep)).toBe(0)
}) })
it('should only remove the dep when the last effect is stopped', () => { it('should only remove the dep when the last effect is stopped', () => {
const obj = reactive({ prop: 1 }) const obj = reactive({ prop: 1 })
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
const runner1 = effect(() => obj.prop) const runner1 = effect(() => obj.prop)
const dep = getDepFromReactive(toRaw(obj), 'prop') const dep = getDepFromReactive(toRaw(obj), 'prop')
expect(dep).toHaveLength(1) expect(getSubCount(dep)).toBe(1)
const runner2 = effect(() => obj.prop) const runner2 = effect(() => obj.prop)
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep) expect(getSubCount(dep)).toBe(2)
expect(dep).toHaveLength(2)
obj.prop = 2 obj.prop = 2
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep) expect(getSubCount(dep)).toBe(2)
expect(dep).toHaveLength(2)
stop(runner1) stop(runner1)
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep) expect(getSubCount(dep)).toBe(1)
expect(dep).toHaveLength(1)
obj.prop = 3 obj.prop = 3
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep) expect(getSubCount(dep)).toBe(1)
expect(dep).toHaveLength(1)
stop(runner2) stop(runner2)
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
obj.prop = 4 obj.prop = 4
runner1() runner1()
runner2() 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', () => { it('should remove the dep when it is no longer used by the effect', () => {
@ -1098,18 +1120,15 @@ describe('reactivity/effect', () => {
b: 2, b: 2,
c: 'a', c: 'a',
}) })
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
effect(() => obj[obj.c]) effect(() => obj[obj.c])
const depC = getDepFromReactive(toRaw(obj), 'c') const depC = getDepFromReactive(toRaw(obj), 'c')
expect(getDepFromReactive(toRaw(obj), 'a')).toHaveLength(1) expect(getSubCount(getDepFromReactive(toRaw(obj), 'a'))).toBe(1)
expect(getDepFromReactive(toRaw(obj), 'b')).toBeUndefined() expect(getSubCount(depC)).toBe(1)
expect(depC).toHaveLength(1)
obj.c = 'b' obj.c = 'b'
obj.a = 4 obj.a = 4
expect(getDepFromReactive(toRaw(obj), 'a')).toBeUndefined() expect(getSubCount(getDepFromReactive(toRaw(obj), 'b'))).toBe(1)
expect(getDepFromReactive(toRaw(obj), 'b')).toHaveLength(1)
expect(getDepFromReactive(toRaw(obj), 'c')).toBe(depC) expect(getDepFromReactive(toRaw(obj), 'c')).toBe(depC)
expect(depC).toHaveLength(1) expect(getSubCount(depC)).toBe(1)
}) })
}) })
}) })

View File

@ -247,16 +247,15 @@ describe('reactivity/effect/scope', () => {
watchEffect(() => { watchEffect(() => {
watchEffectSpy() watchEffectSpy()
r.value r.value
c.value
}) })
}) })
c!.value // computed is lazy so trigger collection
expect(computedSpy).toHaveBeenCalledTimes(1) expect(computedSpy).toHaveBeenCalledTimes(1)
expect(watchSpy).toHaveBeenCalledTimes(0) expect(watchSpy).toHaveBeenCalledTimes(0)
expect(watchEffectSpy).toHaveBeenCalledTimes(1) expect(watchEffectSpy).toHaveBeenCalledTimes(1)
r.value++ r.value++
c!.value
await nextTick() await nextTick()
expect(computedSpy).toHaveBeenCalledTimes(2) expect(computedSpy).toHaveBeenCalledTimes(2)
expect(watchSpy).toHaveBeenCalledTimes(1) expect(watchSpy).toHaveBeenCalledTimes(1)
@ -265,7 +264,6 @@ describe('reactivity/effect/scope', () => {
scope.stop() scope.stop()
r.value++ r.value++
c!.value
await nextTick() await nextTick()
// should not trigger anymore // should not trigger anymore
expect(computedSpy).toHaveBeenCalledTimes(2) expect(computedSpy).toHaveBeenCalledTimes(2)

View File

@ -6,7 +6,7 @@ import {
shallowRef as ref, shallowRef as ref,
toRaw, toRaw,
} from '../src/index' } from '../src/index'
import { getDepFromReactive } from '../src/reactiveEffect' import { getDepFromReactive } from '../src/dep'
describe.skipIf(!global.gc)('reactivity/gc', () => { describe.skipIf(!global.gc)('reactivity/gc', () => {
const gc = () => { const gc = () => {

View File

@ -1,114 +0,0 @@
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
})
}

View File

@ -409,7 +409,7 @@ describe('reactivity/readonly', () => {
const eff = effect(() => { const eff = effect(() => {
roArr.includes(2) 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)', () => { test('readonly should track and trigger if wrapping reactive original (collection)', () => {

View File

@ -442,4 +442,15 @@ describe('reactivity/ref', () => {
expect(a.value).toBe(rr) expect(a.value).toBe(rr)
expect(a.value).not.toBe(r) 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)
})
}) })

View File

@ -160,6 +160,7 @@ describe('shallowReactive', () => {
shallowArray.pop() shallowArray.pop()
expect(size).toBe(0) expect(size).toBe(0)
}) })
test('should not observe when iterating', () => { test('should not observe when iterating', () => {
const shallowArray = shallowReactive<object[]>([]) const shallowArray = shallowReactive<object[]>([])
const a = {} const a = {}

View File

@ -11,13 +11,7 @@ import {
toRaw, toRaw,
} from './reactive' } from './reactive'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants' import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { import { ITERATE_KEY, track, trigger } from './dep'
pauseScheduling,
pauseTracking,
resetScheduling,
resetTracking,
} from './effect'
import { ITERATE_KEY, track, trigger } from './reactiveEffect'
import { import {
hasChanged, hasChanged,
hasOwn, hasOwn,
@ -29,6 +23,7 @@ import {
} from '@vue/shared' } from '@vue/shared'
import { isRef } from './ref' import { isRef } from './ref'
import { warn } from './warning' import { warn } from './warning'
import { endBatch, pauseTracking, resetTracking, startBatch } from './effect'
const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`) const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`)
@ -69,11 +64,11 @@ function createArrayInstrumentations() {
// which leads to infinite loops in some cases (#2137) // which leads to infinite loops in some cases (#2137)
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => { ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) { instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
startBatch()
pauseTracking() pauseTracking()
pauseScheduling()
const res = (toRaw(this) as any)[key].apply(this, args) const res = (toRaw(this) as any)[key].apply(this, args)
resetScheduling()
resetTracking() resetTracking()
endBatch()
return res return res
} }
}) })
@ -133,7 +128,14 @@ class BaseReactiveHandler implements ProxyHandler<Target> {
} }
} }
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)) { if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res return res

View File

@ -1,10 +1,5 @@
import { toRaw, toReactive, toReadonly } from './reactive' import { toRaw, toReactive, toReadonly } from './reactive'
import { import { ITERATE_KEY, MAP_KEY_ITERATE_KEY, track, trigger } from './dep'
ITERATE_KEY,
MAP_KEY_ITERATE_KEY,
track,
trigger,
} from './reactiveEffect'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants' import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { capitalize, hasChanged, hasOwn, isMap, toRawType } from '@vue/shared' import { capitalize, hasChanged, hasOwn, isMap, toRawType } from '@vue/shared'

View File

@ -1,10 +1,18 @@
import { type DebuggerOptions, ReactiveEffect } from './effect' import { isFunction } from '@vue/shared'
import { type Ref, trackRefValue, triggerRefValue } from './ref' import {
import { NOOP, hasChanged, isFunction } from '@vue/shared' type DebuggerEvent,
import { toRaw } from './reactive' type DebuggerOptions,
import type { Dep } from './dep' EffectFlags,
import { DirtyLevels, ReactiveFlags } from './constants' type Link,
type ReactiveEffect,
type Subscriber,
activeSub,
refreshComputed,
} from './effect'
import type { Ref } from './ref'
import { warn } from './warning' import { warn } from './warning'
import { Dep, globalVersion } from './dep'
import { ReactiveFlags, TrackOpTypes } from './constants'
declare const ComputedRefSymbol: unique symbol declare const ComputedRefSymbol: unique symbol
@ -14,7 +22,10 @@ export interface ComputedRef<T = any> extends WritableComputedRef<T> {
} }
export interface WritableComputedRef<T> extends Ref<T> { 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 export type ComputedGetter<T> = (oldValue?: T) => T
@ -25,74 +36,71 @@ export interface WritableComputedOptions<T> {
set: ComputedSetter<T> set: ComputedSetter<T>
} }
export const COMPUTED_SIDE_EFFECT_WARN = /**
`Computed is still dirty after getter evaluation,` + * @internal
` likely because a computed is mutating its own dependency in its getter.` + */
` State mutations in computed getters should be avoided. ` + export class ComputedRefImpl<T = any> implements Subscriber {
` Check the docs for more details: https://vuejs.org/guide/essentials/computed.html#getters-should-be-side-effect-free` // 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
export class ComputedRefImpl<T> { // dev only
public dep?: Dep = undefined onTrack?: (event: DebuggerEvent) => void
// dev only
private _value!: T onTrigger?: (event: DebuggerEvent) => void
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
public _cacheable: boolean
constructor( constructor(
getter: ComputedGetter<T>, public fn: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>, private readonly setter: ComputedSetter<T> | undefined,
isReadonly: boolean, public isSSR: boolean,
isSSR: boolean,
) { ) {
this.effect = new ReactiveEffect( this.__v_isReadonly = !setter
() => getter(this._value), }
() =>
triggerRefValue( notify() {
this, // avoid infinite self recursion
this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect if (activeSub !== this) {
? DirtyLevels.MaybeDirty_ComputedSideEffect this.flags |= EffectFlags.DIRTY
: DirtyLevels.MaybeDirty, this.dep.notify()
), } else if (__DEV__) {
) // TODO warn
this.effect.computed = this }
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
} }
get value() { get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376 const link = __DEV__
const self = toRaw(this) ? this.dep.track({
if ( target: this,
(!self._cacheable || self.effect.dirty) && type: TrackOpTypes.GET,
hasChanged(self._value, (self._value = self.effect.run()!)) key: 'value',
) { })
triggerRefValue(self, DirtyLevels.Dirty) : this.dep.track()
refreshComputed(this)
// sync version after evaluation
if (link) {
link.version = this.dep.version
} }
trackRefValue(self) return this._value
if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) { }
__DEV__ && warn(COMPUTED_SIDE_EFFECT_WARN)
triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect) set value(newValue) {
if (this.setter) {
this.setter(newValue)
} else if (__DEV__) {
warn('Write operation failed: computed value is readonly')
} }
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
}
set _dirty(v) {
this.effect.dirty = v
}
// #endregion
} }
/** /**
@ -142,26 +150,20 @@ export function computed<T>(
isSSR = false, isSSR = false,
) { ) {
let getter: ComputedGetter<T> let getter: ComputedGetter<T>
let setter: ComputedSetter<T> let setter: ComputedSetter<T> | undefined
const onlyGetter = isFunction(getterOrOptions) if (isFunction(getterOrOptions)) {
if (onlyGetter) {
getter = getterOrOptions getter = getterOrOptions
setter = __DEV__
? () => {
warn('Write operation failed: computed value is readonly')
}
: NOOP
} else { } else {
getter = getterOrOptions.get getter = getterOrOptions.get
setter = getterOrOptions.set setter = getterOrOptions.set
} }
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR) const cRef = new ComputedRefImpl(getter, setter, isSSR)
if (__DEV__ && debugOptions && !isSSR) { if (__DEV__ && debugOptions && !isSSR) {
cRef.effect.onTrack = debugOptions.onTrack cRef.onTrack = debugOptions.onTrack
cRef.effect.onTrigger = debugOptions.onTrigger cRef.onTrigger = debugOptions.onTrigger
} }
return cRef as any return cRef as any

View File

@ -1,6 +0,0 @@
import { computed } from './computed'
/**
* @deprecated use `computed` instead. See #5912
*/
export const deferredComputed = computed

View File

@ -1,17 +1,295 @@
import type { ReactiveEffect } from './effect' import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
import type { ComputedRefImpl } from './computed' 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 * Incremented every time a reactive change happens
computed?: ComputedRefImpl<any> * 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()
}
}
} }
export const createDep = ( function addSub(link: Link) {
cleanup: () => void, const computed = link.dep.computed
computed?: ComputedRefImpl<any>, // computed getting its first subscriber
): Dep => { // enable tracking + lazily subscribe to all its deps
const dep = new Map() as Dep if (computed && !link.dep.subs) {
dep.cleanup = cleanup computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
dep.computed = computed for (let l = computed.deps; l; l = l.nextDep) {
return dep 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()
}
/**
* Test only
*/
export function getDepFromReactive(object: any, key: string | number | symbol) {
return targetMap.get(object)?.get(key)
} }

View File

@ -1,17 +1,14 @@
import { NOOP, extend } from '@vue/shared' import { extend, hasChanged } from '@vue/shared'
import type { ComputedRefImpl } from './computed' import type { ComputedRefImpl } from './computed'
import { import type { TrackOpTypes, TriggerOpTypes } from './constants'
DirtyLevels, import { type Dep, globalVersion } from './dep'
type TrackOpTypes, import { recordEffectScope } from './effectScope'
type TriggerOpTypes, import { warn } from './warning'
} from './constants'
import type { Dep } from './dep'
import { type EffectScope, recordEffectScope } from './effectScope'
export type EffectScheduler = (...args: any[]) => any export type EffectScheduler = (...args: any[]) => any
export type DebuggerEvent = { export type DebuggerEvent = {
effect: ReactiveEffect effect: Subscriber
} & DebuggerEventExtraInfo } & DebuggerEventExtraInfo
export type DebuggerEventExtraInfo = { export type DebuggerEventExtraInfo = {
@ -23,154 +20,13 @@ export type DebuggerEventExtraInfo = {
oldTarget?: Map<any, any> | Set<any> oldTarget?: Map<any, any> | Set<any>
} }
export let activeEffect: ReactiveEffect | undefined
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
/**
* Can be attached after creation
* @internal
*/
computed?: ComputedRefImpl<T>
/**
* @internal
*/
allowRecurse?: boolean
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
/**
* @internal
*/
_dirtyLevel = DirtyLevels.Dirty
/**
* @internal
*/
_trackId = 0
/**
* @internal
*/
_runnings = 0
/**
* @internal
*/
_shouldSchedule = false
/**
* @internal
*/
_depsLength = 0
constructor(
public fn: () => T,
public trigger: () => void,
public scheduler?: EffectScheduler,
scope?: EffectScope,
) {
recordEffectScope(this, scope)
}
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
}
public set dirty(v) {
this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty
}
run() {
this._dirtyLevel = DirtyLevels.NotDirty
if (!this.active) {
return this.fn()
}
let lastShouldTrack = shouldTrack
let lastEffect = activeEffect
try {
shouldTrack = true
activeEffect = this
this._runnings++
preCleanupEffect(this)
return this.fn()
} finally {
postCleanupEffect(this)
this._runnings--
activeEffect = lastEffect
shouldTrack = lastShouldTrack
}
}
stop() {
if (this.active) {
preCleanupEffect(this)
postCleanupEffect(this)
this.onStop?.()
this.active = false
}
}
}
function triggerComputed(computed: ComputedRefImpl<any>) {
return computed.value
}
function preCleanupEffect(effect: ReactiveEffect) {
effect._trackId++
effect._depsLength = 0
}
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)
}
effect.deps.length = effect._depsLength
}
}
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()
}
}
}
export interface DebuggerOptions { export interface DebuggerOptions {
onTrack?: (event: DebuggerEvent) => void onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void
} }
export interface ReactiveEffectOptions extends DebuggerOptions { export interface ReactiveEffectOptions extends DebuggerOptions {
lazy?: boolean
scheduler?: EffectScheduler scheduler?: EffectScheduler
scope?: EffectScope
allowRecurse?: boolean allowRecurse?: boolean
onStop?: () => void onStop?: () => void
} }
@ -180,38 +36,409 @@ export interface ReactiveEffectRunner<T = any> {
effect: ReactiveEffect 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,
}
/** /**
* Registers the given function to track reactive updates. * Subscriber is a type that tracks (or subscribes to) a list of deps.
*
* 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 interface Subscriber extends DebuggerOptions {
/**
* Head of the doubly linked list representing the deps
* @internal
*/
deps?: Link
/**
* Tail of the same list
* @internal
*/
depsTail?: Link
/**
* @internal
*/
flags: EffectFlags
/**
* @internal
*/
notify(): 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
*/
deps?: Link = undefined
/**
* @internal
*/
depsTail?: Link = undefined
/**
* @internal
*/
flags: EffectFlags = EffectFlags.ACTIVE | EffectFlags.TRACKING
/**
* @internal
*/
nextEffect?: ReactiveEffect = undefined
/**
* @internal
*/
allowRecurse?: boolean
scheduler?: EffectScheduler = undefined
onStop?: () => void
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
constructor(public fn: () => T) {
recordEffectScope(this)
}
/**
* @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() {
// TODO cleanupEffect
if (!(this.flags & EffectFlags.ACTIVE)) {
// stopped during cleanup
return this.fn()
}
this.flags |= EffectFlags.RUNNING
prepareDeps(this)
const prevEffect = activeSub
const prevShouldTrack = shouldTrack
activeSub = this
shouldTrack = true
try {
return this.fn()
} finally {
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.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 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 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 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
}
}
// @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
}
/**
* 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
}
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)
}
}
}
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> {
(): T
effect: ReactiveEffect
}
export function effect<T = any>( export function effect<T = any>(
fn: () => T, fn: () => T,
options?: ReactiveEffectOptions, options?: ReactiveEffectOptions,
): ReactiveEffectRunner { ): ReactiveEffectRunner<T> {
if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) { if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
fn = (fn as ReactiveEffectRunner).effect.fn fn = (fn as ReactiveEffectRunner).effect.fn
} }
const _effect = new ReactiveEffect(fn, NOOP, () => { const e = new ReactiveEffect(fn)
if (_effect.dirty) {
_effect.run()
}
})
if (options) { if (options) {
extend(_effect, options) extend(e, options)
if (options.scope) recordEffectScope(_effect, options.scope)
} }
if (!options || !options.lazy) { try {
_effect.run() e.run()
} catch (err) {
e.stop()
throw err
} }
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner const runner = e.run.bind(e) as ReactiveEffectRunner
runner.effect = _effect runner.effect = e
return runner return runner
} }
@ -224,9 +451,10 @@ export function stop(runner: ReactiveEffectRunner) {
runner.effect.stop() runner.effect.stop()
} }
/**
* @internal
*/
export let shouldTrack = true export let shouldTrack = true
export let pauseScheduleStack = 0
const trackStack: boolean[] = [] const trackStack: boolean[] = []
/** /**
@ -252,76 +480,3 @@ export function resetTracking() {
const last = trackStack.pop() const last = trackStack.pop()
shouldTrack = last === undefined ? true : last 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()
}

View File

@ -44,16 +44,14 @@ export {
type ComputedGetter, type ComputedGetter,
type ComputedSetter, type ComputedSetter,
} from './computed' } from './computed'
export { deferredComputed } from './deferredComputed'
export { export {
effect, effect,
stop, stop,
enableTracking, enableTracking,
pauseTracking, pauseTracking,
resetTracking, resetTracking,
pauseScheduling,
resetScheduling,
ReactiveEffect, ReactiveEffect,
EffectFlags,
type ReactiveEffectRunner, type ReactiveEffectRunner,
type ReactiveEffectOptions, type ReactiveEffectOptions,
type EffectScheduler, type EffectScheduler,
@ -61,7 +59,7 @@ export {
type DebuggerEvent, type DebuggerEvent,
type DebuggerEventExtraInfo, type DebuggerEventExtraInfo,
} from './effect' } from './effect'
export { trigger, track, ITERATE_KEY } from './reactiveEffect' export { trigger, track, ITERATE_KEY } from './dep'
export { export {
effectScope, effectScope,
EffectScope, EffectScope,

View File

@ -1,150 +0,0 @@
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)
}

View File

@ -1,11 +1,3 @@
import type { ComputedRef } from './computed'
import {
activeEffect,
shouldTrack,
trackEffect,
triggerEffects,
} from './effect'
import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants'
import { import {
type IfAny, type IfAny,
hasChanged, hasChanged,
@ -13,7 +5,9 @@ import {
isFunction, isFunction,
isObject, isObject,
} from '@vue/shared' } from '@vue/shared'
import { Dep, getDepFromReactive } from './dep'
import { import {
type ShallowReactiveMarker,
isProxy, isProxy,
isReactive, isReactive,
isReadonly, isReadonly,
@ -21,10 +15,8 @@ import {
toRaw, toRaw,
toReactive, toReactive,
} from './reactive' } from './reactive'
import type { ShallowReactiveMarker } from './reactive' import type { ComputedRef } from './computed'
import { type Dep, createDep } from './dep' import { TrackOpTypes, TriggerOpTypes } from './constants'
import { ComputedRefImpl } from './computed'
import { getDepFromReactive } from './reactiveEffect'
declare const RefSymbol: unique symbol declare const RefSymbol: unique symbol
export declare const RawSymbol: unique symbol export declare const RawSymbol: unique symbol
@ -39,54 +31,6 @@ export interface Ref<T = any> {
[RefSymbol]: true [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. * Checks if a value is a ref object.
* *
@ -95,7 +39,7 @@ export function triggerRefValue(
*/ */
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T> export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref { export function isRef(r: any): r is Ref {
return !!(r && r.__v_isRef === true) return r ? r.__v_isRef === true : false
} }
/** /**
@ -151,11 +95,15 @@ function createRef(rawValue: unknown, shallow: boolean) {
return new RefImpl(rawValue, shallow) return new RefImpl(rawValue, shallow)
} }
class RefImpl<T> { /**
private _value: T * @internal
*/
class RefImpl<T = any> {
_value: T
private _rawValue: T private _rawValue: T
public dep?: Dep = undefined dep: Dep = new Dep()
public readonly __v_isRef = true public readonly __v_isRef = true
constructor( constructor(
@ -167,18 +115,37 @@ class RefImpl<T> {
} }
get value() { get value() {
trackRefValue(this) if (__DEV__) {
this.dep.track({
target: this,
type: TrackOpTypes.GET,
key: 'value',
})
} else {
this.dep.track()
}
return this._value return this._value
} }
set value(newVal) { set value(newValue) {
const oldValue = this._rawValue
const useDirectValue = const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal) this.__v_isShallow || isShallow(newValue) || isReadonly(newValue)
newVal = useDirectValue ? newVal : toRaw(newVal) newValue = useDirectValue ? newValue : toRaw(newValue)
if (hasChanged(newVal, this._rawValue)) { if (hasChanged(newValue, oldValue)) {
this._rawValue = newVal this._rawValue = newValue
this._value = useDirectValue ? newVal : toReactive(newVal) this._value = useDirectValue ? newValue : toReactive(newValue)
triggerRefValue(this, DirtyLevels.Dirty, newVal) if (__DEV__) {
this.dep.trigger({
target: this,
type: TriggerOpTypes.SET,
key: 'value',
newValue,
oldValue,
})
} else {
this.dep.trigger()
}
} }
} }
} }
@ -209,7 +176,16 @@ class RefImpl<T> {
* @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref} * @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref}
*/ */
export function triggerRef(ref: Ref) { 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> export type MaybeRef<T = any> = T | Ref<T>
@ -295,7 +271,7 @@ export type CustomRefFactory<T> = (
} }
class CustomRefImpl<T> { class CustomRefImpl<T> {
public dep?: Dep = undefined public dep: Dep
private readonly _get: ReturnType<CustomRefFactory<T>>['get'] private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set'] private readonly _set: ReturnType<CustomRefFactory<T>>['set']
@ -303,10 +279,8 @@ class CustomRefImpl<T> {
public readonly __v_isRef = true public readonly __v_isRef = true
constructor(factory: CustomRefFactory<T>) { constructor(factory: CustomRefFactory<T>) {
const { get, set } = factory( const dep = (this.dep = new Dep())
() => trackRefValue(this), const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep))
() => triggerRefValue(this),
)
this._get = get this._get = get
this._set = set this._set = set
} }

View File

@ -1,6 +1,5 @@
import { import {
type ComponentInternalInstance, type ComponentInternalInstance,
type ComputedRef,
type SetupContext, type SetupContext,
Suspense, Suspense,
computed, computed,
@ -26,6 +25,8 @@ import {
withAsyncContext, withAsyncContext,
withDefaults, withDefaults,
} from '../src/apiSetupHelpers' } from '../src/apiSetupHelpers'
import type { ComputedRefImpl } from '../../reactivity/src/computed'
import { EffectFlags, type ReactiveEffectRunner, effect } from '@vue/reactivity'
describe('SFC <script setup> helpers', () => { describe('SFC <script setup> helpers', () => {
test('should warn runtime usage', () => { test('should warn runtime usage', () => {
@ -426,7 +427,8 @@ describe('SFC <script setup> helpers', () => {
resolve = r resolve = r
}) })
let c: ComputedRef let c: ComputedRefImpl
let e: ReactiveEffectRunner
const Comp = defineComponent({ const Comp = defineComponent({
async setup() { async setup() {
@ -435,10 +437,11 @@ describe('SFC <script setup> helpers', () => {
__temp = await __temp __temp = await __temp
__restore() __restore()
c = computed(() => {}) c = computed(() => {}) as unknown as ComputedRefImpl
e = effect(() => c.value)
// register the lifecycle after an await statement // register the lifecycle after an await statement
onMounted(resolve) onMounted(resolve)
return () => '' return () => c.value
}, },
}) })
@ -447,10 +450,12 @@ describe('SFC <script setup> helpers', () => {
app.mount(root) app.mount(root)
await ready await ready
expect(c!.effect.active).toBe(true) expect(e!.effect.flags & EffectFlags.ACTIVE).toBeTruthy()
expect(c!.flags & EffectFlags.TRACKING).toBeTruthy()
app.unmount() app.unmount()
expect(c!.effect.active).toBe(false) expect(e!.effect.flags & EffectFlags.ACTIVE).toBeFalsy()
expect(c!.flags & EffectFlags.TRACKING).toBeFalsy()
}) })
}) })
}) })

View File

@ -23,6 +23,7 @@ import {
} from '@vue/runtime-test' } from '@vue/runtime-test'
import { import {
type DebuggerEvent, type DebuggerEvent,
EffectFlags,
ITERATE_KEY, ITERATE_KEY,
type Ref, type Ref,
type ShallowRef, type ShallowRef,
@ -1185,7 +1186,7 @@ describe('api: watch', () => {
await nextTick() await nextTick()
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 ', () => { test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
@ -1474,4 +1475,46 @@ describe('api: watch', () => {
unwatch!() unwatch!()
expect(scope.effects.length).toBe(0) 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)
})
}) })

View File

@ -187,7 +187,6 @@ export function defineAsyncComponent<
if (instance.parent && isKeepAlive(instance.parent.vnode)) { if (instance.parent && isKeepAlive(instance.parent.vnode)) {
// parent is keep-alive, force update so the loaded component's // parent is keep-alive, force update so the loaded component's
// name is taken into account // name is taken into account
instance.parent.effect.dirty = true
queueJob(instance.parent.update) queueJob(instance.parent.update)
} }
}) })

View File

@ -1,6 +1,7 @@
import { import {
type ComputedRef, type ComputedRef,
type DebuggerOptions, type DebuggerOptions,
EffectFlags,
type EffectScheduler, type EffectScheduler,
ReactiveEffect, ReactiveEffect,
ReactiveFlags, ReactiveFlags,
@ -337,8 +338,11 @@ function doWatch(
let oldValue: any = isMultiSource let oldValue: any = isMultiSource
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE) ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
: INITIAL_WATCHER_VALUE : INITIAL_WATCHER_VALUE
const job: SchedulerJob = () => { const job: SchedulerJob = (immediateFirstRun?: boolean) => {
if (!effect.active || !effect.dirty) { if (
!(effect.flags & EffectFlags.ACTIVE) ||
(!effect.dirty && !immediateFirstRun)
) {
return return
} }
if (cb) { if (cb) {
@ -380,8 +384,11 @@ function doWatch(
// it is allowed to self-trigger (#1727) // it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb job.allowRecurse = !!cb
const effect = new ReactiveEffect(getter)
let scheduler: EffectScheduler let scheduler: EffectScheduler
if (flush === 'sync') { if (flush === 'sync') {
effect.flags |= EffectFlags.NO_BATCH
scheduler = job as any // the scheduler function gets called directly scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') { } else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
@ -391,8 +398,7 @@ function doWatch(
if (instance) job.id = instance.uid if (instance) job.id = instance.uid
scheduler = () => queueJob(job) scheduler = () => queueJob(job)
} }
effect.scheduler = scheduler
const effect = new ReactiveEffect(getter, NOOP, scheduler)
const scope = getCurrentScope() const scope = getCurrentScope()
const unwatch = () => { const unwatch = () => {
@ -410,7 +416,7 @@ function doWatch(
// initial run // initial run
if (cb) { if (cb) {
if (immediate) { if (immediate) {
job() job(true)
} else { } else {
oldValue = effect.run() oldValue = effect.run()
} }

View File

@ -286,9 +286,13 @@ export interface ComponentInternalInstance {
*/ */
effect: ReactiveEffect 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. * The render function that returns vdom tree.
* @internal * @internal
@ -552,6 +556,7 @@ export function createComponentInstance(
subTree: null!, // will be set synchronously right after creation subTree: null!, // will be set synchronously right after creation
effect: null!, effect: null!,
update: null!, // will be set synchronously right after creation update: null!, // will be set synchronously right after creation
job: null!,
scope: new EffectScope(true /* detached */), scope: new EffectScope(true /* detached */),
render: null, render: null,
proxy: null, proxy: null,

View File

@ -276,7 +276,6 @@ export const publicPropertiesMap: PublicPropertiesMap =
$forceUpdate: i => $forceUpdate: i =>
i.f || i.f ||
(i.f = () => { (i.f = () => {
i.effect.dirty = true
queueJob(i.update) queueJob(i.update)
}), }),
$nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)), $nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),

View File

@ -245,8 +245,7 @@ const BaseTransitionImpl: ComponentOptions = {
state.isLeaving = false state.isLeaving = false
// #6835 // #6835
// it also needs to be updated when active is undefined // it also needs to be updated when active is undefined
if (instance.update.active !== false) { if (instance.job.active !== false) {
instance.effect.dirty = true
instance.update() instance.update()
} }
} }

View File

@ -38,7 +38,8 @@ export function initCustomFormatter() {
{}, {},
['span', vueStyle, genRefFlag(obj)], ['span', vueStyle, genRefFlag(obj)],
'<', '<',
formatValue(obj.value), // avoid debugger accessing value affecting behavior
formatValue('_value' in obj ? obj._value : obj),
`>`, `>`,
] ]
} else if (isReactive(obj)) { } else if (isReactive(obj)) {

View File

@ -93,7 +93,6 @@ function rerender(id: string, newRender?: Function) {
instance.renderCache = [] instance.renderCache = []
// this flag forces child components with slot content to update // this flag forces child components with slot content to update
isHmrUpdating = true isHmrUpdating = true
instance.effect.dirty = true
instance.update() instance.update()
isHmrUpdating = false isHmrUpdating = false
}) })
@ -138,7 +137,6 @@ function reload(id: string, newComp: HMRComponent) {
// 4. Force the parent instance to re-render. This will cause all updated // 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 // 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. // don't end up forcing the same parent to re-render multiple times.
instance.parent.effect.dirty = true
queueJob(instance.parent.update) queueJob(instance.parent.update)
} else if (instance.appContext.reload) { } else if (instance.appContext.reload) {
// root instance mounted via createApp() has a reload method // root instance mounted via createApp() has a reload method

View File

@ -1289,7 +1289,6 @@ function baseCreateRenderer(
// double updating the same child component in the same flush. // double updating the same child component in the same flush.
invalidateJob(instance.update) invalidateJob(instance.update)
// instance.update is the reactive effect. // instance.update is the reactive effect.
instance.effect.dirty = true
instance.update() instance.update()
} }
} else { } else {
@ -1574,19 +1573,15 @@ function baseCreateRenderer(
} }
// create reactive effect for rendering // create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect( instance.scope.on()
componentUpdateFn, const effect = (instance.effect = new ReactiveEffect(componentUpdateFn))
NOOP, instance.scope.off()
() => queueJob(update),
instance.scope, // track it in component's effect scope 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)
const update: SchedulerJob = (instance.update = () => {
if (effect.dirty) {
effect.run()
}
})
update.id = instance.uid
// allowRecurse // allowRecurse
// #1801, #2043 component render effects should allow recursive updates // #1801, #2043 component render effects should allow recursive updates
toggleRecurse(instance, true) toggleRecurse(instance, true)
@ -1598,7 +1593,7 @@ function baseCreateRenderer(
effect.onTrigger = instance.rtg effect.onTrigger = instance.rtg
? e => invokeArrayFns(instance.rtg!, e) ? e => invokeArrayFns(instance.rtg!, e)
: void 0 : void 0
update.ownerInstance = instance job.ownerInstance = instance
} }
update() update()
@ -2265,7 +2260,7 @@ function baseCreateRenderer(
unregisterHMR(instance) unregisterHMR(instance)
} }
const { bum, scope, update, subTree, um } = instance const { bum, scope, job, subTree, um } = instance
// beforeUnmount hook // beforeUnmount hook
if (bum) { if (bum) {
@ -2282,11 +2277,11 @@ function baseCreateRenderer(
// stop effects in component scope // stop effects in component scope
scope.stop() 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. // setup has resolved.
if (update) { if (job) {
// so that scheduler will no longer invoke it // so that scheduler will no longer invoke it
update.active = false job.active = false
unmount(subTree, instance, parentSuspense, doRemove) unmount(subTree, instance, parentSuspense, doRemove)
} }
// unmounted hook // unmounted hook
@ -2421,10 +2416,10 @@ function resolveChildrenNamespace(
} }
function toggleRecurse( function toggleRecurse(
{ effect, update }: ComponentInternalInstance, { effect, job }: ComponentInternalInstance,
allowed: boolean, allowed: boolean,
) { ) {
effect.allowRecurse = update.allowRecurse = allowed effect.allowRecurse = job.allowRecurse = allowed
} }
export function needTransition( export function needTransition(

View File

@ -6,7 +6,6 @@ export interface SchedulerJob extends Function {
id?: number id?: number
pre?: boolean pre?: boolean
active?: boolean active?: boolean
computed?: boolean
/** /**
* Indicates whether the effect is allowed to recursively trigger itself * Indicates whether the effect is allowed to recursively trigger itself
* when managed by the scheduler. * when managed by the scheduler.

View File

@ -1,4 +1,4 @@
import { computed, createSSRApp, defineComponent, h, reactive } from 'vue' import { computed, createSSRApp, defineComponent, h, reactive, ref } from 'vue'
import { renderToString } from '../src/renderToString' import { renderToString } from '../src/renderToString'
// #5208 reported memory leak of keeping computed alive during SSR // #5208 reported memory leak of keeping computed alive during SSR
@ -45,3 +45,23 @@ test('computed reactivity during SSR', async () => {
// during the render phase // during the render phase
expect(getterSpy).toHaveBeenCalledTimes(2) 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')
})

View File

@ -143,15 +143,6 @@ function renderComponentSubTree(
comp.ssrRender = ssrCompile(comp.template, instance) 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 const ssrRender = instance.ssrRender || comp.ssrRender
if (ssrRender) { if (ssrRender) {
// optimized // optimized