feat(reactivity): more efficient reactivity system (#5912)
fix #311, fix #1811, fix #6018, fix #7160, fix #8714, fix #9149, fix #9419, fix #9464
This commit is contained in:
parent
feb2f2edce
commit
16e06ca08f
|
@ -184,7 +184,7 @@ describe('reactivity/computed', () => {
|
||||||
// mutate n
|
// mutate n
|
||||||
n.value++
|
n.value++
|
||||||
// on the 2nd run, plusOne.value should have already updated.
|
// on the 2nd run, plusOne.value should have already updated.
|
||||||
expect(plusOneValues).toMatchObject([1, 2, 2])
|
expect(plusOneValues).toMatchObject([1, 2])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should warn if trying to set a readonly computed', () => {
|
it('should warn if trying to set a readonly computed', () => {
|
||||||
|
@ -288,4 +288,167 @@ describe('reactivity/computed', () => {
|
||||||
oldValue: 2
|
oldValue: 2
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// https://github.com/vuejs/core/pull/5912#issuecomment-1497596875
|
||||||
|
it('should query deps dirty sequentially', () => {
|
||||||
|
const cSpy = vi.fn()
|
||||||
|
|
||||||
|
const a = ref<null | { v: number }>({
|
||||||
|
v: 1
|
||||||
|
})
|
||||||
|
const b = computed(() => {
|
||||||
|
return a.value
|
||||||
|
})
|
||||||
|
const c = computed(() => {
|
||||||
|
cSpy()
|
||||||
|
return b.value?.v
|
||||||
|
})
|
||||||
|
const d = computed(() => {
|
||||||
|
if (b.value) {
|
||||||
|
return c.value
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
d.value
|
||||||
|
a.value!.v = 2
|
||||||
|
a.value = null
|
||||||
|
d.value
|
||||||
|
expect(cSpy).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// https://github.com/vuejs/core/pull/5912#issuecomment-1738257692
|
||||||
|
it('chained computed dirty reallocation after querying dirty', () => {
|
||||||
|
let _msg: string | undefined
|
||||||
|
|
||||||
|
const items = ref<number[]>()
|
||||||
|
const isLoaded = computed(() => {
|
||||||
|
return !!items.value
|
||||||
|
})
|
||||||
|
const msg = computed(() => {
|
||||||
|
if (isLoaded.value) {
|
||||||
|
return 'The items are loaded'
|
||||||
|
} else {
|
||||||
|
return 'The items are not loaded'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
_msg = msg.value
|
||||||
|
})
|
||||||
|
|
||||||
|
items.value = [1, 2, 3]
|
||||||
|
items.value = [1, 2, 3]
|
||||||
|
items.value = undefined
|
||||||
|
|
||||||
|
expect(_msg).toBe('The items are not loaded')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('chained computed dirty reallocation after trigger computed getter', () => {
|
||||||
|
let _msg: string | undefined
|
||||||
|
|
||||||
|
const items = ref<number[]>()
|
||||||
|
const isLoaded = computed(() => {
|
||||||
|
return !!items.value
|
||||||
|
})
|
||||||
|
const msg = computed(() => {
|
||||||
|
if (isLoaded.value) {
|
||||||
|
return 'The items are loaded'
|
||||||
|
} else {
|
||||||
|
return 'The items are not loaded'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
_msg = msg.value
|
||||||
|
items.value = [1, 2, 3]
|
||||||
|
isLoaded.value // <- trigger computed getter
|
||||||
|
_msg = msg.value
|
||||||
|
items.value = undefined
|
||||||
|
_msg = msg.value
|
||||||
|
|
||||||
|
expect(_msg).toBe('The items are not loaded')
|
||||||
|
})
|
||||||
|
|
||||||
|
// https://github.com/vuejs/core/pull/5912#issuecomment-1739159832
|
||||||
|
it('deps order should be consistent with the last time get value', () => {
|
||||||
|
const cSpy = vi.fn()
|
||||||
|
|
||||||
|
const a = ref(0)
|
||||||
|
const b = computed(() => {
|
||||||
|
return a.value % 3 !== 0
|
||||||
|
})
|
||||||
|
const c = computed(() => {
|
||||||
|
cSpy()
|
||||||
|
if (a.value % 3 === 2) {
|
||||||
|
return 'expensive'
|
||||||
|
}
|
||||||
|
return 'cheap'
|
||||||
|
})
|
||||||
|
const d = computed(() => {
|
||||||
|
return a.value % 3 === 2
|
||||||
|
})
|
||||||
|
const e = computed(() => {
|
||||||
|
if (b.value) {
|
||||||
|
if (d.value) {
|
||||||
|
return 'Avoiding expensive calculation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.value
|
||||||
|
})
|
||||||
|
|
||||||
|
e.value
|
||||||
|
a.value++
|
||||||
|
e.value
|
||||||
|
|
||||||
|
expect(e.effect.deps.length).toBe(3)
|
||||||
|
expect(e.effect.deps.indexOf((b as any).dep)).toBe(0)
|
||||||
|
expect(e.effect.deps.indexOf((d as any).dep)).toBe(1)
|
||||||
|
expect(e.effect.deps.indexOf((c as any).dep)).toBe(2)
|
||||||
|
expect(cSpy).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
|
a.value++
|
||||||
|
e.value
|
||||||
|
|
||||||
|
expect(cSpy).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should trigger by the second computed that maybe dirty', () => {
|
||||||
|
const cSpy = vi.fn()
|
||||||
|
|
||||||
|
const src1 = ref(0)
|
||||||
|
const src2 = ref(0)
|
||||||
|
const c1 = computed(() => src1.value)
|
||||||
|
const c2 = computed(() => (src1.value % 2) + src2.value)
|
||||||
|
const c3 = computed(() => {
|
||||||
|
cSpy()
|
||||||
|
c1.value
|
||||||
|
c2.value
|
||||||
|
})
|
||||||
|
|
||||||
|
c3.value
|
||||||
|
src1.value = 2
|
||||||
|
c3.value
|
||||||
|
expect(cSpy).toHaveBeenCalledTimes(2)
|
||||||
|
src2.value = 1
|
||||||
|
c3.value
|
||||||
|
expect(cSpy).toHaveBeenCalledTimes(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should trigger the second effect', () => {
|
||||||
|
const fnSpy = vi.fn()
|
||||||
|
const v = ref(1)
|
||||||
|
const c = computed(() => v.value)
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
c.value
|
||||||
|
})
|
||||||
|
effect(() => {
|
||||||
|
c.value
|
||||||
|
fnSpy()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(fnSpy).toBeCalledTimes(1)
|
||||||
|
v.value = 2
|
||||||
|
expect(fnSpy).toBeCalledTimes(2)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,57 +1,32 @@
|
||||||
import { computed, deferredComputed, effect, ref } from '../src'
|
import { computed, effect, ref } from '../src'
|
||||||
|
|
||||||
describe('deferred computed', () => {
|
describe('deferred computed', () => {
|
||||||
const tick = Promise.resolve()
|
test('should not trigger if value did not change', () => {
|
||||||
|
|
||||||
test('should only trigger once on multiple mutations', async () => {
|
|
||||||
const src = ref(0)
|
const src = ref(0)
|
||||||
const c = deferredComputed(() => src.value)
|
const c = computed(() => src.value % 2)
|
||||||
const spy = vi.fn()
|
const spy = vi.fn()
|
||||||
effect(() => {
|
effect(() => {
|
||||||
spy(c.value)
|
spy(c.value)
|
||||||
})
|
})
|
||||||
expect(spy).toHaveBeenCalledTimes(1)
|
expect(spy).toHaveBeenCalledTimes(1)
|
||||||
src.value = 1
|
|
||||||
src.value = 2
|
|
||||||
src.value = 3
|
|
||||||
// not called yet
|
|
||||||
expect(spy).toHaveBeenCalledTimes(1)
|
|
||||||
await tick
|
|
||||||
// should only trigger once
|
|
||||||
expect(spy).toHaveBeenCalledTimes(2)
|
|
||||||
expect(spy).toHaveBeenCalledWith(c.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should not trigger if value did not change', async () => {
|
|
||||||
const src = ref(0)
|
|
||||||
const c = deferredComputed(() => src.value % 2)
|
|
||||||
const spy = vi.fn()
|
|
||||||
effect(() => {
|
|
||||||
spy(c.value)
|
|
||||||
})
|
|
||||||
expect(spy).toHaveBeenCalledTimes(1)
|
|
||||||
src.value = 1
|
|
||||||
src.value = 2
|
src.value = 2
|
||||||
|
|
||||||
await tick
|
|
||||||
// should not trigger
|
// should not trigger
|
||||||
expect(spy).toHaveBeenCalledTimes(1)
|
expect(spy).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
src.value = 3
|
src.value = 3
|
||||||
src.value = 4
|
|
||||||
src.value = 5
|
src.value = 5
|
||||||
await tick
|
|
||||||
// should trigger because latest value changes
|
// should trigger because latest value changes
|
||||||
expect(spy).toHaveBeenCalledTimes(2)
|
expect(spy).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('chained computed trigger', async () => {
|
test('chained computed trigger', () => {
|
||||||
const effectSpy = vi.fn()
|
const effectSpy = vi.fn()
|
||||||
const c1Spy = vi.fn()
|
const c1Spy = vi.fn()
|
||||||
const c2Spy = vi.fn()
|
const c2Spy = vi.fn()
|
||||||
|
|
||||||
const src = ref(0)
|
const src = ref(0)
|
||||||
const c1 = deferredComputed(() => {
|
const c1 = computed(() => {
|
||||||
c1Spy()
|
c1Spy()
|
||||||
return src.value % 2
|
return src.value % 2
|
||||||
})
|
})
|
||||||
|
@ -69,19 +44,18 @@ describe('deferred computed', () => {
|
||||||
expect(effectSpy).toHaveBeenCalledTimes(1)
|
expect(effectSpy).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
src.value = 1
|
src.value = 1
|
||||||
await tick
|
|
||||||
expect(c1Spy).toHaveBeenCalledTimes(2)
|
expect(c1Spy).toHaveBeenCalledTimes(2)
|
||||||
expect(c2Spy).toHaveBeenCalledTimes(2)
|
expect(c2Spy).toHaveBeenCalledTimes(2)
|
||||||
expect(effectSpy).toHaveBeenCalledTimes(2)
|
expect(effectSpy).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('chained computed avoid re-compute', async () => {
|
test('chained computed avoid re-compute', () => {
|
||||||
const effectSpy = vi.fn()
|
const effectSpy = vi.fn()
|
||||||
const c1Spy = vi.fn()
|
const c1Spy = vi.fn()
|
||||||
const c2Spy = vi.fn()
|
const c2Spy = vi.fn()
|
||||||
|
|
||||||
const src = ref(0)
|
const src = ref(0)
|
||||||
const c1 = deferredComputed(() => {
|
const c1 = computed(() => {
|
||||||
c1Spy()
|
c1Spy()
|
||||||
return src.value % 2
|
return src.value % 2
|
||||||
})
|
})
|
||||||
|
@ -98,26 +72,24 @@ describe('deferred computed', () => {
|
||||||
src.value = 2
|
src.value = 2
|
||||||
src.value = 4
|
src.value = 4
|
||||||
src.value = 6
|
src.value = 6
|
||||||
await tick
|
expect(c1Spy).toHaveBeenCalledTimes(4)
|
||||||
// c1 should re-compute once.
|
|
||||||
expect(c1Spy).toHaveBeenCalledTimes(2)
|
|
||||||
// c2 should not have to re-compute because c1 did not change.
|
// c2 should not have to re-compute because c1 did not change.
|
||||||
expect(c2Spy).toHaveBeenCalledTimes(1)
|
expect(c2Spy).toHaveBeenCalledTimes(1)
|
||||||
// effect should not trigger because c2 did not change.
|
// effect should not trigger because c2 did not change.
|
||||||
expect(effectSpy).toHaveBeenCalledTimes(1)
|
expect(effectSpy).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('chained computed value invalidation', async () => {
|
test('chained computed value invalidation', () => {
|
||||||
const effectSpy = vi.fn()
|
const effectSpy = vi.fn()
|
||||||
const c1Spy = vi.fn()
|
const c1Spy = vi.fn()
|
||||||
const c2Spy = vi.fn()
|
const c2Spy = vi.fn()
|
||||||
|
|
||||||
const src = ref(0)
|
const src = ref(0)
|
||||||
const c1 = deferredComputed(() => {
|
const c1 = computed(() => {
|
||||||
c1Spy()
|
c1Spy()
|
||||||
return src.value % 2
|
return src.value % 2
|
||||||
})
|
})
|
||||||
const c2 = deferredComputed(() => {
|
const c2 = computed(() => {
|
||||||
c2Spy()
|
c2Spy()
|
||||||
return c1.value + 1
|
return c1.value + 1
|
||||||
})
|
})
|
||||||
|
@ -139,17 +111,17 @@ describe('deferred computed', () => {
|
||||||
expect(c2Spy).toHaveBeenCalledTimes(2)
|
expect(c2Spy).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('sync access of invalidated chained computed should not prevent final effect from running', async () => {
|
test('sync access of invalidated chained computed should not prevent final effect from running', () => {
|
||||||
const effectSpy = vi.fn()
|
const effectSpy = vi.fn()
|
||||||
const c1Spy = vi.fn()
|
const c1Spy = vi.fn()
|
||||||
const c2Spy = vi.fn()
|
const c2Spy = vi.fn()
|
||||||
|
|
||||||
const src = ref(0)
|
const src = ref(0)
|
||||||
const c1 = deferredComputed(() => {
|
const c1 = computed(() => {
|
||||||
c1Spy()
|
c1Spy()
|
||||||
return src.value % 2
|
return src.value % 2
|
||||||
})
|
})
|
||||||
const c2 = deferredComputed(() => {
|
const c2 = computed(() => {
|
||||||
c2Spy()
|
c2Spy()
|
||||||
return c1.value + 1
|
return c1.value + 1
|
||||||
})
|
})
|
||||||
|
@ -162,14 +134,13 @@ describe('deferred computed', () => {
|
||||||
src.value = 1
|
src.value = 1
|
||||||
// sync access c2
|
// sync access c2
|
||||||
c2.value
|
c2.value
|
||||||
await tick
|
|
||||||
expect(effectSpy).toHaveBeenCalledTimes(2)
|
expect(effectSpy).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should not compute if deactivated before scheduler is called', async () => {
|
test('should not compute if deactivated before scheduler is called', () => {
|
||||||
const c1Spy = vi.fn()
|
const c1Spy = vi.fn()
|
||||||
const src = ref(0)
|
const src = ref(0)
|
||||||
const c1 = deferredComputed(() => {
|
const c1 = computed(() => {
|
||||||
c1Spy()
|
c1Spy()
|
||||||
return src.value % 2
|
return src.value % 2
|
||||||
})
|
})
|
||||||
|
@ -179,7 +150,6 @@ describe('deferred computed', () => {
|
||||||
c1.effect.stop()
|
c1.effect.stop()
|
||||||
// trigger
|
// trigger
|
||||||
src.value++
|
src.value++
|
||||||
await tick
|
|
||||||
expect(c1Spy).toHaveBeenCalledTimes(1)
|
expect(c1Spy).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
ref,
|
|
||||||
reactive,
|
reactive,
|
||||||
effect,
|
effect,
|
||||||
stop,
|
stop,
|
||||||
|
@ -12,7 +11,8 @@ import {
|
||||||
readonly,
|
readonly,
|
||||||
ReactiveEffectRunner
|
ReactiveEffectRunner
|
||||||
} from '../src/index'
|
} from '../src/index'
|
||||||
import { ITERATE_KEY } from '../src/effect'
|
import { pauseScheduling, resetScheduling } from '../src/effect'
|
||||||
|
import { ITERATE_KEY, getDepFromReactive } from '../src/reactiveEffect'
|
||||||
|
|
||||||
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)', () => {
|
||||||
|
@ -574,8 +574,8 @@ describe('reactivity/effect', () => {
|
||||||
expect(output.fx2).toBe(1 + 3 + 3)
|
expect(output.fx2).toBe(1 + 3 + 3)
|
||||||
expect(fx1Spy).toHaveBeenCalledTimes(1)
|
expect(fx1Spy).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
// Invoked twice due to change of fx1.
|
// Invoked due to change of fx1.
|
||||||
expect(fx2Spy).toHaveBeenCalledTimes(2)
|
expect(fx2Spy).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
fx1Spy.mockClear()
|
fx1Spy.mockClear()
|
||||||
fx2Spy.mockClear()
|
fx2Spy.mockClear()
|
||||||
|
@ -821,26 +821,6 @@ describe('reactivity/effect', () => {
|
||||||
expect(dummy).toBe(3)
|
expect(dummy).toBe(3)
|
||||||
})
|
})
|
||||||
|
|
||||||
// #5707
|
|
||||||
// when an effect completes its run, it should clear the tracking bits of
|
|
||||||
// its tracked deps. However, if the effect stops itself, the deps list is
|
|
||||||
// emptied so their bits are never cleared.
|
|
||||||
it('edge case: self-stopping effect tracking ref', () => {
|
|
||||||
const c = ref(true)
|
|
||||||
const runner = effect(() => {
|
|
||||||
// reference ref
|
|
||||||
if (!c.value) {
|
|
||||||
// stop itself while running
|
|
||||||
stop(runner)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// trigger run
|
|
||||||
c.value = !c.value
|
|
||||||
// should clear bits
|
|
||||||
expect((c as any).dep.w).toBe(0)
|
|
||||||
expect((c as any).dep.n).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('events: onStop', () => {
|
it('events: onStop', () => {
|
||||||
const onStop = vi.fn()
|
const onStop = vi.fn()
|
||||||
const runner = effect(() => {}, {
|
const runner = effect(() => {}, {
|
||||||
|
@ -1015,4 +995,83 @@ describe('reactivity/effect', () => {
|
||||||
expect(has).toBe(false)
|
expect(has).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should be triggered once with pauseScheduling', () => {
|
||||||
|
const counter = reactive({ num: 0 })
|
||||||
|
|
||||||
|
const counterSpy = vi.fn(() => counter.num)
|
||||||
|
effect(counterSpy)
|
||||||
|
|
||||||
|
counterSpy.mockClear()
|
||||||
|
|
||||||
|
pauseScheduling()
|
||||||
|
counter.num++
|
||||||
|
counter.num++
|
||||||
|
resetScheduling()
|
||||||
|
expect(counterSpy).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('empty dep cleanup', () => {
|
||||||
|
it('should remove the dep when the effect is stopped', () => {
|
||||||
|
const obj = reactive({ prop: 1 })
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||||
|
const runner = effect(() => obj.prop)
|
||||||
|
const dep = getDepFromReactive(toRaw(obj), 'prop')
|
||||||
|
expect(dep).toHaveLength(1)
|
||||||
|
obj.prop = 2
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
|
||||||
|
expect(dep).toHaveLength(1)
|
||||||
|
stop(runner)
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||||
|
obj.prop = 3
|
||||||
|
runner()
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should only remove the dep when the last effect is stopped', () => {
|
||||||
|
const obj = reactive({ prop: 1 })
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||||
|
const runner1 = effect(() => obj.prop)
|
||||||
|
const dep = getDepFromReactive(toRaw(obj), 'prop')
|
||||||
|
expect(dep).toHaveLength(1)
|
||||||
|
const runner2 = effect(() => obj.prop)
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
|
||||||
|
expect(dep).toHaveLength(2)
|
||||||
|
obj.prop = 2
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
|
||||||
|
expect(dep).toHaveLength(2)
|
||||||
|
stop(runner1)
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
|
||||||
|
expect(dep).toHaveLength(1)
|
||||||
|
obj.prop = 3
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
|
||||||
|
expect(dep).toHaveLength(1)
|
||||||
|
stop(runner2)
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||||
|
obj.prop = 4
|
||||||
|
runner1()
|
||||||
|
runner2()
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove the dep when it is no longer used by the effect', () => {
|
||||||
|
const obj = reactive<{ a: number; b: number; c: 'a' | 'b' }>({
|
||||||
|
a: 1,
|
||||||
|
b: 2,
|
||||||
|
c: 'a'
|
||||||
|
})
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
|
||||||
|
effect(() => obj[obj.c])
|
||||||
|
const depC = getDepFromReactive(toRaw(obj), 'c')
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'a')).toHaveLength(1)
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'b')).toBeUndefined()
|
||||||
|
expect(depC).toHaveLength(1)
|
||||||
|
obj.c = 'b'
|
||||||
|
obj.a = 4
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'a')).toBeUndefined()
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'b')).toHaveLength(1)
|
||||||
|
expect(getDepFromReactive(toRaw(obj), 'c')).toBe(depC)
|
||||||
|
expect(depC).toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
import {
|
||||||
|
ComputedRef,
|
||||||
|
computed,
|
||||||
|
effect,
|
||||||
|
reactive,
|
||||||
|
shallowRef as ref,
|
||||||
|
toRaw
|
||||||
|
} from '../src/index'
|
||||||
|
import { getDepFromReactive } from '../src/reactiveEffect'
|
||||||
|
|
||||||
|
describe.skipIf(!global.gc)('reactivity/gc', () => {
|
||||||
|
const gc = () => {
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
global.gc!()
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// #9233
|
||||||
|
it('should release computed cache', async () => {
|
||||||
|
const src = ref<{} | undefined>({})
|
||||||
|
const srcRef = new WeakRef(src.value!)
|
||||||
|
|
||||||
|
let c: ComputedRef | undefined = computed(() => src.value)
|
||||||
|
|
||||||
|
c.value // cache src value
|
||||||
|
src.value = undefined // release value
|
||||||
|
c = undefined // release computed
|
||||||
|
|
||||||
|
await gc()
|
||||||
|
expect(srcRef.deref()).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should release reactive property dep', async () => {
|
||||||
|
const src = reactive({ foo: 1 })
|
||||||
|
|
||||||
|
let c: ComputedRef | undefined = computed(() => src.foo)
|
||||||
|
|
||||||
|
c.value
|
||||||
|
expect(getDepFromReactive(toRaw(src), 'foo')).not.toBeUndefined()
|
||||||
|
|
||||||
|
c = undefined
|
||||||
|
await gc()
|
||||||
|
await gc()
|
||||||
|
expect(getDepFromReactive(toRaw(src), 'foo')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not release effect for ref', async () => {
|
||||||
|
const spy = vi.fn()
|
||||||
|
const src = ref(0)
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
spy()
|
||||||
|
src.value
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
await gc()
|
||||||
|
src.value++
|
||||||
|
expect(spy).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not release effect for reactive', async () => {
|
||||||
|
const spy = vi.fn()
|
||||||
|
const src = reactive({ foo: 1 })
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
spy()
|
||||||
|
src.foo
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
await gc()
|
||||||
|
src.foo++
|
||||||
|
expect(spy).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
|
@ -99,6 +99,39 @@ describe('reactivity/reactive/Array', () => {
|
||||||
expect(fn).toHaveBeenCalledTimes(1)
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('shift on Array should trigger dependency once', () => {
|
||||||
|
const arr = reactive([1, 2, 3])
|
||||||
|
const fn = vi.fn()
|
||||||
|
effect(() => {
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
arr[i]
|
||||||
|
}
|
||||||
|
fn()
|
||||||
|
})
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
|
arr.shift()
|
||||||
|
expect(fn).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
//#6018
|
||||||
|
test('edge case: avoid trigger effect in deleteProperty when array length-decrease mutation methods called', () => {
|
||||||
|
const arr = ref([1])
|
||||||
|
const fn1 = vi.fn()
|
||||||
|
const fn2 = vi.fn()
|
||||||
|
effect(() => {
|
||||||
|
fn1()
|
||||||
|
if (arr.value.length > 0) {
|
||||||
|
arr.value.slice()
|
||||||
|
fn2()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(fn1).toHaveBeenCalledTimes(1)
|
||||||
|
expect(fn2).toHaveBeenCalledTimes(1)
|
||||||
|
arr.value.splice(0)
|
||||||
|
expect(fn1).toHaveBeenCalledTimes(2)
|
||||||
|
expect(fn2).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
test('add existing index on Array should not trigger length dependency', () => {
|
test('add existing index on Array should not trigger length dependency', () => {
|
||||||
const array = new Array(3)
|
const array = new Array(3)
|
||||||
const observed = reactive(array)
|
const observed = reactive(array)
|
||||||
|
|
|
@ -2,7 +2,6 @@ import {
|
||||||
reactive,
|
reactive,
|
||||||
readonly,
|
readonly,
|
||||||
toRaw,
|
toRaw,
|
||||||
ReactiveFlags,
|
|
||||||
Target,
|
Target,
|
||||||
readonlyMap,
|
readonlyMap,
|
||||||
reactiveMap,
|
reactiveMap,
|
||||||
|
@ -11,14 +10,14 @@ import {
|
||||||
isReadonly,
|
isReadonly,
|
||||||
isShallow
|
isShallow
|
||||||
} from './reactive'
|
} from './reactive'
|
||||||
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
|
||||||
import {
|
import {
|
||||||
track,
|
|
||||||
trigger,
|
|
||||||
ITERATE_KEY,
|
|
||||||
pauseTracking,
|
pauseTracking,
|
||||||
resetTracking
|
resetTracking,
|
||||||
|
pauseScheduling,
|
||||||
|
resetScheduling
|
||||||
} from './effect'
|
} from './effect'
|
||||||
|
import { track, trigger, ITERATE_KEY } from './reactiveEffect'
|
||||||
import {
|
import {
|
||||||
isObject,
|
isObject,
|
||||||
hasOwn,
|
hasOwn,
|
||||||
|
@ -71,7 +70,9 @@ function createArrayInstrumentations() {
|
||||||
;(['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[]) {
|
||||||
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()
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import { toRaw, ReactiveFlags, toReactive, toReadonly } from './reactive'
|
import { toRaw, toReactive, toReadonly } from './reactive'
|
||||||
import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
|
import {
|
||||||
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
track,
|
||||||
|
trigger,
|
||||||
|
ITERATE_KEY,
|
||||||
|
MAP_KEY_ITERATE_KEY
|
||||||
|
} from './reactiveEffect'
|
||||||
|
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
|
||||||
import { capitalize, hasOwn, hasChanged, toRawType, isMap } from '@vue/shared'
|
import { capitalize, hasOwn, hasChanged, toRawType, isMap } from '@vue/shared'
|
||||||
|
|
||||||
export type CollectionTypes = IterableCollections | WeakCollections
|
export type CollectionTypes = IterableCollections | WeakCollections
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { DebuggerOptions, ReactiveEffect } from './effect'
|
import { DebuggerOptions, ReactiveEffect } from './effect'
|
||||||
import { Ref, trackRefValue, triggerRefValue } from './ref'
|
import { Ref, trackRefValue, triggerRefValue } from './ref'
|
||||||
import { isFunction, NOOP } from '@vue/shared'
|
import { hasChanged, isFunction, NOOP } from '@vue/shared'
|
||||||
import { ReactiveFlags, toRaw } from './reactive'
|
import { toRaw } from './reactive'
|
||||||
import { Dep } from './dep'
|
import { Dep } from './dep'
|
||||||
|
import { DirtyLevels, ReactiveFlags } from './constants'
|
||||||
|
|
||||||
declare const ComputedRefSymbol: unique symbol
|
declare const ComputedRefSymbol: unique symbol
|
||||||
|
|
||||||
|
@ -32,7 +33,6 @@ export class ComputedRefImpl<T> {
|
||||||
public readonly __v_isRef = true
|
public readonly __v_isRef = true
|
||||||
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
|
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
|
||||||
|
|
||||||
public _dirty = true
|
|
||||||
public _cacheable: boolean
|
public _cacheable: boolean
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -42,10 +42,7 @@ export class ComputedRefImpl<T> {
|
||||||
isSSR: boolean
|
isSSR: boolean
|
||||||
) {
|
) {
|
||||||
this.effect = new ReactiveEffect(getter, () => {
|
this.effect = new ReactiveEffect(getter, () => {
|
||||||
if (!this._dirty) {
|
triggerRefValue(this, DirtyLevels.ComputedValueMaybeDirty)
|
||||||
this._dirty = true
|
|
||||||
triggerRefValue(this)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
this.effect.computed = this
|
this.effect.computed = this
|
||||||
this.effect.active = this._cacheable = !isSSR
|
this.effect.active = this._cacheable = !isSSR
|
||||||
|
@ -56,9 +53,10 @@ export class ComputedRefImpl<T> {
|
||||||
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
|
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
|
||||||
const self = toRaw(this)
|
const self = toRaw(this)
|
||||||
trackRefValue(self)
|
trackRefValue(self)
|
||||||
if (self._dirty || !self._cacheable) {
|
if (!self._cacheable || self.effect.dirty) {
|
||||||
self._dirty = false
|
if (hasChanged(self._value, (self._value = self.effect.run()!))) {
|
||||||
self._value = self.effect.run()!
|
triggerRefValue(self, DirtyLevels.ComputedValueDirty)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return self._value
|
return self._value
|
||||||
}
|
}
|
||||||
|
@ -66,6 +64,16 @@ export class ComputedRefImpl<T> {
|
||||||
set value(newValue: T) {
|
set value(newValue: T) {
|
||||||
this._setter(newValue)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
// using literal strings instead of numbers so that it's easier to inspect
|
||||||
|
// debugger events
|
||||||
|
|
||||||
|
export const enum TrackOpTypes {
|
||||||
|
GET = 'get',
|
||||||
|
HAS = 'has',
|
||||||
|
ITERATE = 'iterate'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum TriggerOpTypes {
|
||||||
|
SET = 'set',
|
||||||
|
ADD = 'add',
|
||||||
|
DELETE = 'delete',
|
||||||
|
CLEAR = 'clear'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum ReactiveFlags {
|
||||||
|
SKIP = '__v_skip',
|
||||||
|
IS_REACTIVE = '__v_isReactive',
|
||||||
|
IS_READONLY = '__v_isReadonly',
|
||||||
|
IS_SHALLOW = '__v_isShallow',
|
||||||
|
RAW = '__v_raw'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum DirtyLevels {
|
||||||
|
NotDirty = 0,
|
||||||
|
ComputedValueMaybeDirty = 1,
|
||||||
|
ComputedValueDirty = 2,
|
||||||
|
Dirty = 3
|
||||||
|
}
|
|
@ -1,88 +1,6 @@
|
||||||
import { Dep } from './dep'
|
import { computed } from './computed'
|
||||||
import { ReactiveEffect } from './effect'
|
|
||||||
import { ComputedGetter, ComputedRef } from './computed'
|
|
||||||
import { ReactiveFlags, toRaw } from './reactive'
|
|
||||||
import { trackRefValue, triggerRefValue } from './ref'
|
|
||||||
|
|
||||||
const tick = /*#__PURE__*/ Promise.resolve()
|
/**
|
||||||
const queue: any[] = []
|
* @deprecated use `computed` instead. See #5912
|
||||||
let queued = false
|
*/
|
||||||
|
export const deferredComputed = computed
|
||||||
const scheduler = (fn: any) => {
|
|
||||||
queue.push(fn)
|
|
||||||
if (!queued) {
|
|
||||||
queued = true
|
|
||||||
tick.then(flush)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const flush = () => {
|
|
||||||
for (let i = 0; i < queue.length; i++) {
|
|
||||||
queue[i]()
|
|
||||||
}
|
|
||||||
queue.length = 0
|
|
||||||
queued = false
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeferredComputedRefImpl<T> {
|
|
||||||
public dep?: Dep = undefined
|
|
||||||
|
|
||||||
private _value!: T
|
|
||||||
private _dirty = true
|
|
||||||
public readonly effect: ReactiveEffect<T>
|
|
||||||
|
|
||||||
public readonly __v_isRef = true
|
|
||||||
public readonly [ReactiveFlags.IS_READONLY] = true
|
|
||||||
|
|
||||||
constructor(getter: ComputedGetter<T>) {
|
|
||||||
let compareTarget: any
|
|
||||||
let hasCompareTarget = false
|
|
||||||
let scheduled = false
|
|
||||||
this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => {
|
|
||||||
if (this.dep) {
|
|
||||||
if (computedTrigger) {
|
|
||||||
compareTarget = this._value
|
|
||||||
hasCompareTarget = true
|
|
||||||
} else if (!scheduled) {
|
|
||||||
const valueToCompare = hasCompareTarget ? compareTarget : this._value
|
|
||||||
scheduled = true
|
|
||||||
hasCompareTarget = false
|
|
||||||
scheduler(() => {
|
|
||||||
if (this.effect.active && this._get() !== valueToCompare) {
|
|
||||||
triggerRefValue(this)
|
|
||||||
}
|
|
||||||
scheduled = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// chained upstream computeds are notified synchronously to ensure
|
|
||||||
// value invalidation in case of sync access; normal effects are
|
|
||||||
// deferred to be triggered in scheduler.
|
|
||||||
for (const e of this.dep) {
|
|
||||||
if (e.computed instanceof DeferredComputedRefImpl) {
|
|
||||||
e.scheduler!(true /* computedTrigger */)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._dirty = true
|
|
||||||
})
|
|
||||||
this.effect.computed = this as any
|
|
||||||
}
|
|
||||||
|
|
||||||
private _get() {
|
|
||||||
if (this._dirty) {
|
|
||||||
this._dirty = false
|
|
||||||
return (this._value = this.effect.run()!)
|
|
||||||
}
|
|
||||||
return this._value
|
|
||||||
}
|
|
||||||
|
|
||||||
get value() {
|
|
||||||
trackRefValue(this)
|
|
||||||
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
|
|
||||||
return toRaw(this)._get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deferredComputed<T>(getter: () => T): ComputedRef<T> {
|
|
||||||
return new DeferredComputedRefImpl(getter) as any
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,57 +1,17 @@
|
||||||
import { ReactiveEffect, trackOpBit } from './effect'
|
import type { ReactiveEffect } from './effect'
|
||||||
|
import type { ComputedRefImpl } from './computed'
|
||||||
|
|
||||||
export type Dep = Set<ReactiveEffect> & TrackedMarkers
|
export type Dep = Map<ReactiveEffect, number> & {
|
||||||
|
cleanup: () => void
|
||||||
/**
|
computed?: ComputedRefImpl<any>
|
||||||
* wasTracked and newTracked maintain the status for several levels of effect
|
|
||||||
* tracking recursion. One bit per level is used to define whether the dependency
|
|
||||||
* was/is tracked.
|
|
||||||
*/
|
|
||||||
type TrackedMarkers = {
|
|
||||||
/**
|
|
||||||
* wasTracked
|
|
||||||
*/
|
|
||||||
w: number
|
|
||||||
/**
|
|
||||||
* newTracked
|
|
||||||
*/
|
|
||||||
n: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createDep = (effects?: ReactiveEffect[]): Dep => {
|
export const createDep = (
|
||||||
const dep = new Set<ReactiveEffect>(effects) as Dep
|
cleanup: () => void,
|
||||||
dep.w = 0
|
computed?: ComputedRefImpl<any>
|
||||||
dep.n = 0
|
): Dep => {
|
||||||
|
const dep = new Map() as Dep
|
||||||
|
dep.cleanup = cleanup
|
||||||
|
dep.computed = computed
|
||||||
return dep
|
return dep
|
||||||
}
|
}
|
||||||
|
|
||||||
export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
|
|
||||||
|
|
||||||
export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0
|
|
||||||
|
|
||||||
export const initDepMarkers = ({ deps }: ReactiveEffect) => {
|
|
||||||
if (deps.length) {
|
|
||||||
for (let i = 0; i < deps.length; i++) {
|
|
||||||
deps[i].w |= trackOpBit // set was tracked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const finalizeDepMarkers = (effect: ReactiveEffect) => {
|
|
||||||
const { deps } = effect
|
|
||||||
if (deps.length) {
|
|
||||||
let ptr = 0
|
|
||||||
for (let i = 0; i < deps.length; i++) {
|
|
||||||
const dep = deps[i]
|
|
||||||
if (wasTracked(dep) && !newTracked(dep)) {
|
|
||||||
dep.delete(effect)
|
|
||||||
} else {
|
|
||||||
deps[ptr++] = dep
|
|
||||||
}
|
|
||||||
// clear bits
|
|
||||||
dep.w &= ~trackOpBit
|
|
||||||
dep.n &= ~trackOpBit
|
|
||||||
}
|
|
||||||
deps.length = ptr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,34 +1,8 @@
|
||||||
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
import { NOOP, extend } from '@vue/shared'
|
||||||
import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
|
import type { ComputedRefImpl } from './computed'
|
||||||
|
import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants'
|
||||||
|
import type { Dep } from './dep'
|
||||||
import { EffectScope, recordEffectScope } from './effectScope'
|
import { EffectScope, recordEffectScope } from './effectScope'
|
||||||
import {
|
|
||||||
createDep,
|
|
||||||
Dep,
|
|
||||||
finalizeDepMarkers,
|
|
||||||
initDepMarkers,
|
|
||||||
newTracked,
|
|
||||||
wasTracked
|
|
||||||
} from './dep'
|
|
||||||
import { ComputedRefImpl } from './computed'
|
|
||||||
|
|
||||||
// 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 Sets to reduce memory overhead.
|
|
||||||
type KeyToDepMap = Map<any, Dep>
|
|
||||||
const targetMap = new WeakMap<object, KeyToDepMap>()
|
|
||||||
|
|
||||||
// The number of effects currently being tracked recursively.
|
|
||||||
let effectTrackDepth = 0
|
|
||||||
|
|
||||||
export let trackOpBit = 1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The bitwise track markers support at most 30 levels of recursion.
|
|
||||||
* This value is chosen to enable modern JS engines to use a SMI on all platforms.
|
|
||||||
* When recursion depth is greater, fall back to using a full cleanup.
|
|
||||||
*/
|
|
||||||
const maxMarkerBits = 30
|
|
||||||
|
|
||||||
export type EffectScheduler = (...args: any[]) => any
|
export type EffectScheduler = (...args: any[]) => any
|
||||||
|
|
||||||
|
@ -47,13 +21,9 @@ export type DebuggerEventExtraInfo = {
|
||||||
|
|
||||||
export let activeEffect: ReactiveEffect | undefined
|
export let activeEffect: ReactiveEffect | undefined
|
||||||
|
|
||||||
export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
|
|
||||||
export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '')
|
|
||||||
|
|
||||||
export class ReactiveEffect<T = any> {
|
export class ReactiveEffect<T = any> {
|
||||||
active = true
|
active = true
|
||||||
deps: Dep[] = []
|
deps: Dep[] = []
|
||||||
parent: ReactiveEffect | undefined = undefined
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can be attached after creation
|
* Can be attached after creation
|
||||||
|
@ -64,10 +34,6 @@ export class ReactiveEffect<T = any> {
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
allowRecurse?: boolean
|
allowRecurse?: boolean
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
private deferStop?: boolean
|
|
||||||
|
|
||||||
onStop?: () => void
|
onStop?: () => void
|
||||||
// dev only
|
// dev only
|
||||||
|
@ -75,77 +41,115 @@ export class ReactiveEffect<T = any> {
|
||||||
// dev only
|
// dev only
|
||||||
onTrigger?: (event: DebuggerEvent) => void
|
onTrigger?: (event: DebuggerEvent) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_dirtyLevel = DirtyLevels.Dirty
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_trackId = 0
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_runnings = 0
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_queryings = 0
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_depsLength = 0
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public fn: () => T,
|
public fn: () => T,
|
||||||
public scheduler: EffectScheduler | null = null,
|
public trigger: () => void,
|
||||||
|
public scheduler?: EffectScheduler,
|
||||||
scope?: EffectScope
|
scope?: EffectScope
|
||||||
) {
|
) {
|
||||||
recordEffectScope(this, scope)
|
recordEffectScope(this, scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get dirty() {
|
||||||
|
if (this._dirtyLevel === DirtyLevels.ComputedValueMaybeDirty) {
|
||||||
|
this._dirtyLevel = DirtyLevels.NotDirty
|
||||||
|
this._queryings++
|
||||||
|
pauseTracking()
|
||||||
|
for (const dep of this.deps) {
|
||||||
|
if (dep.computed) {
|
||||||
|
triggerComputed(dep.computed)
|
||||||
|
if (this._dirtyLevel >= DirtyLevels.ComputedValueDirty) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resetTracking()
|
||||||
|
this._queryings--
|
||||||
|
}
|
||||||
|
return this._dirtyLevel >= DirtyLevels.ComputedValueDirty
|
||||||
|
}
|
||||||
|
|
||||||
|
public set dirty(v) {
|
||||||
|
this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty
|
||||||
|
}
|
||||||
|
|
||||||
run() {
|
run() {
|
||||||
|
this._dirtyLevel = DirtyLevels.NotDirty
|
||||||
if (!this.active) {
|
if (!this.active) {
|
||||||
return this.fn()
|
return this.fn()
|
||||||
}
|
}
|
||||||
let parent: ReactiveEffect | undefined = activeEffect
|
|
||||||
let lastShouldTrack = shouldTrack
|
let lastShouldTrack = shouldTrack
|
||||||
while (parent) {
|
let lastEffect = activeEffect
|
||||||
if (parent === this) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
parent = parent.parent
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
this.parent = activeEffect
|
|
||||||
activeEffect = this
|
|
||||||
shouldTrack = true
|
shouldTrack = true
|
||||||
|
activeEffect = this
|
||||||
trackOpBit = 1 << ++effectTrackDepth
|
this._runnings++
|
||||||
|
preCleanupEffect(this)
|
||||||
if (effectTrackDepth <= maxMarkerBits) {
|
|
||||||
initDepMarkers(this)
|
|
||||||
} else {
|
|
||||||
cleanupEffect(this)
|
|
||||||
}
|
|
||||||
return this.fn()
|
return this.fn()
|
||||||
} finally {
|
} finally {
|
||||||
if (effectTrackDepth <= maxMarkerBits) {
|
postCleanupEffect(this)
|
||||||
finalizeDepMarkers(this)
|
this._runnings--
|
||||||
}
|
activeEffect = lastEffect
|
||||||
|
|
||||||
trackOpBit = 1 << --effectTrackDepth
|
|
||||||
|
|
||||||
activeEffect = this.parent
|
|
||||||
shouldTrack = lastShouldTrack
|
shouldTrack = lastShouldTrack
|
||||||
this.parent = undefined
|
|
||||||
|
|
||||||
if (this.deferStop) {
|
|
||||||
this.stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
// stopped while running itself - defer the cleanup
|
if (this.active) {
|
||||||
if (activeEffect === this) {
|
preCleanupEffect(this)
|
||||||
this.deferStop = true
|
postCleanupEffect(this)
|
||||||
} else if (this.active) {
|
this.onStop?.()
|
||||||
cleanupEffect(this)
|
|
||||||
if (this.onStop) {
|
|
||||||
this.onStop()
|
|
||||||
}
|
|
||||||
this.active = false
|
this.active = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupEffect(effect: ReactiveEffect) {
|
function triggerComputed(computed: ComputedRefImpl<any>) {
|
||||||
const { deps } = effect
|
return computed.value
|
||||||
if (deps.length) {
|
}
|
||||||
for (let i = 0; i < deps.length; i++) {
|
|
||||||
deps[i].delete(effect)
|
function preCleanupEffect(effect: ReactiveEffect) {
|
||||||
|
effect._trackId++
|
||||||
|
effect._depsLength = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function postCleanupEffect(effect: ReactiveEffect) {
|
||||||
|
if (effect.deps && 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()
|
||||||
}
|
}
|
||||||
deps.length = 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,7 +189,11 @@ export function effect<T = any>(
|
||||||
fn = (fn as ReactiveEffectRunner).effect.fn
|
fn = (fn as ReactiveEffectRunner).effect.fn
|
||||||
}
|
}
|
||||||
|
|
||||||
const _effect = new ReactiveEffect(fn)
|
const _effect = new ReactiveEffect(fn, NOOP, () => {
|
||||||
|
if (_effect.dirty) {
|
||||||
|
_effect.run()
|
||||||
|
}
|
||||||
|
})
|
||||||
if (options) {
|
if (options) {
|
||||||
extend(_effect, options)
|
extend(_effect, options)
|
||||||
if (options.scope) recordEffectScope(_effect, options.scope)
|
if (options.scope) recordEffectScope(_effect, options.scope)
|
||||||
|
@ -208,6 +216,8 @@ export function stop(runner: ReactiveEffectRunner) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export let shouldTrack = true
|
export let shouldTrack = true
|
||||||
|
export let pauseScheduleStack = 0
|
||||||
|
|
||||||
const trackStack: boolean[] = []
|
const trackStack: boolean[] = []
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -234,196 +244,70 @@ export function resetTracking() {
|
||||||
shouldTrack = last === undefined ? true : last
|
shouldTrack = last === undefined ? true : last
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function pauseScheduling() {
|
||||||
* Tracks access to a reactive property.
|
pauseScheduleStack++
|
||||||
*
|
}
|
||||||
* 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()))
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventInfo = __DEV__
|
export function resetScheduling() {
|
||||||
? { effect: activeEffect, target, type, key }
|
pauseScheduleStack--
|
||||||
: undefined
|
while (!pauseScheduleStack && queueEffectSchedulers.length) {
|
||||||
|
queueEffectSchedulers.shift()!()
|
||||||
trackEffects(dep, eventInfo)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function trackEffects(
|
export function trackEffect(
|
||||||
|
effect: ReactiveEffect,
|
||||||
dep: Dep,
|
dep: Dep,
|
||||||
debuggerEventExtraInfo?: DebuggerEventExtraInfo
|
debuggerEventExtraInfo?: DebuggerEventExtraInfo
|
||||||
) {
|
) {
|
||||||
let shouldTrack = false
|
if (dep.get(effect) !== effect._trackId) {
|
||||||
if (effectTrackDepth <= maxMarkerBits) {
|
dep.set(effect, effect._trackId)
|
||||||
if (!newTracked(dep)) {
|
const oldDep = effect.deps[effect._depsLength]
|
||||||
dep.n |= trackOpBit // set newly tracked
|
if (oldDep !== dep) {
|
||||||
shouldTrack = !wasTracked(dep)
|
if (oldDep) {
|
||||||
}
|
cleanupDepEffect(oldDep, effect)
|
||||||
} else {
|
|
||||||
// Full cleanup mode.
|
|
||||||
shouldTrack = !dep.has(activeEffect!)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldTrack) {
|
|
||||||
dep.add(activeEffect!)
|
|
||||||
activeEffect!.deps.push(dep)
|
|
||||||
if (__DEV__ && activeEffect!.onTrack) {
|
|
||||||
activeEffect!.onTrack(
|
|
||||||
extend(
|
|
||||||
{
|
|
||||||
effect: activeEffect!
|
|
||||||
},
|
|
||||||
debuggerEventExtraInfo!
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventInfo = __DEV__
|
|
||||||
? { target, type, key, newValue, oldValue, oldTarget }
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
if (deps.length === 1) {
|
|
||||||
if (deps[0]) {
|
|
||||||
if (__DEV__) {
|
|
||||||
triggerEffects(deps[0], eventInfo)
|
|
||||||
} else {
|
|
||||||
triggerEffects(deps[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const effects: ReactiveEffect[] = []
|
|
||||||
for (const dep of deps) {
|
|
||||||
if (dep) {
|
|
||||||
effects.push(...dep)
|
|
||||||
}
|
}
|
||||||
|
effect.deps[effect._depsLength++] = dep
|
||||||
|
} else {
|
||||||
|
effect._depsLength++
|
||||||
}
|
}
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
triggerEffects(createDep(effects), eventInfo)
|
effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!))
|
||||||
} else {
|
|
||||||
triggerEffects(createDep(effects))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const queueEffectSchedulers: (() => void)[] = []
|
||||||
|
|
||||||
export function triggerEffects(
|
export function triggerEffects(
|
||||||
dep: Dep | ReactiveEffect[],
|
dep: Dep,
|
||||||
|
dirtyLevel: DirtyLevels,
|
||||||
debuggerEventExtraInfo?: DebuggerEventExtraInfo
|
debuggerEventExtraInfo?: DebuggerEventExtraInfo
|
||||||
) {
|
) {
|
||||||
// spread into array for stabilization
|
pauseScheduling()
|
||||||
const effects = isArray(dep) ? dep : [...dep]
|
for (const effect of dep.keys()) {
|
||||||
for (const effect of effects) {
|
if (!effect.allowRecurse && effect._runnings) {
|
||||||
if (effect.computed) {
|
continue
|
||||||
triggerEffect(effect, debuggerEventExtraInfo)
|
}
|
||||||
}
|
if (
|
||||||
}
|
effect._dirtyLevel < dirtyLevel &&
|
||||||
for (const effect of effects) {
|
(!effect._runnings || dirtyLevel !== DirtyLevels.ComputedValueDirty)
|
||||||
if (!effect.computed) {
|
) {
|
||||||
triggerEffect(effect, debuggerEventExtraInfo)
|
const lastDirtyLevel = effect._dirtyLevel
|
||||||
|
effect._dirtyLevel = dirtyLevel
|
||||||
|
if (
|
||||||
|
lastDirtyLevel === DirtyLevels.NotDirty &&
|
||||||
|
(!effect._queryings || dirtyLevel !== DirtyLevels.ComputedValueDirty)
|
||||||
|
) {
|
||||||
|
if (__DEV__) {
|
||||||
|
effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
|
||||||
|
}
|
||||||
|
effect.trigger()
|
||||||
|
if (effect.scheduler) {
|
||||||
|
queueEffectSchedulers.push(effect.scheduler)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
resetScheduling()
|
||||||
|
|
||||||
function triggerEffect(
|
|
||||||
effect: ReactiveEffect,
|
|
||||||
debuggerEventExtraInfo?: DebuggerEventExtraInfo
|
|
||||||
) {
|
|
||||||
if (effect !== activeEffect || effect.allowRecurse) {
|
|
||||||
if (__DEV__ && effect.onTrigger) {
|
|
||||||
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
|
|
||||||
}
|
|
||||||
if (effect.scheduler) {
|
|
||||||
effect.scheduler()
|
|
||||||
} else {
|
|
||||||
effect.run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDepFromReactive(object: any, key: string | number | symbol) {
|
|
||||||
return targetMap.get(object)?.get(key)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,6 @@ export {
|
||||||
shallowReadonly,
|
shallowReadonly,
|
||||||
markRaw,
|
markRaw,
|
||||||
toRaw,
|
toRaw,
|
||||||
ReactiveFlags /* @remove */,
|
|
||||||
type Raw,
|
type Raw,
|
||||||
type DeepReadonly,
|
type DeepReadonly,
|
||||||
type ShallowReactive,
|
type ShallowReactive,
|
||||||
|
@ -49,12 +48,11 @@ export { deferredComputed } from './deferredComputed'
|
||||||
export {
|
export {
|
||||||
effect,
|
effect,
|
||||||
stop,
|
stop,
|
||||||
trigger,
|
|
||||||
track,
|
|
||||||
enableTracking,
|
enableTracking,
|
||||||
pauseTracking,
|
pauseTracking,
|
||||||
resetTracking,
|
resetTracking,
|
||||||
ITERATE_KEY,
|
pauseScheduling,
|
||||||
|
resetScheduling,
|
||||||
ReactiveEffect,
|
ReactiveEffect,
|
||||||
type ReactiveEffectRunner,
|
type ReactiveEffectRunner,
|
||||||
type ReactiveEffectOptions,
|
type ReactiveEffectOptions,
|
||||||
|
@ -63,6 +61,7 @@ export {
|
||||||
type DebuggerEvent,
|
type DebuggerEvent,
|
||||||
type DebuggerEventExtraInfo
|
type DebuggerEventExtraInfo
|
||||||
} from './effect'
|
} from './effect'
|
||||||
|
export { trigger, track, ITERATE_KEY } from './reactiveEffect'
|
||||||
export {
|
export {
|
||||||
effectScope,
|
effectScope,
|
||||||
EffectScope,
|
EffectScope,
|
||||||
|
@ -71,5 +70,6 @@ export {
|
||||||
} from './effectScope'
|
} from './effectScope'
|
||||||
export {
|
export {
|
||||||
TrackOpTypes /* @remove */,
|
TrackOpTypes /* @remove */,
|
||||||
TriggerOpTypes /* @remove */
|
TriggerOpTypes /* @remove */,
|
||||||
} from './operations'
|
ReactiveFlags /* @remove */
|
||||||
|
} from './constants'
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
// using literal strings instead of numbers so that it's easier to inspect
|
|
||||||
// debugger events
|
|
||||||
|
|
||||||
export const enum TrackOpTypes {
|
|
||||||
GET = 'get',
|
|
||||||
HAS = 'has',
|
|
||||||
ITERATE = 'iterate'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum TriggerOpTypes {
|
|
||||||
SET = 'set',
|
|
||||||
ADD = 'add',
|
|
||||||
DELETE = 'delete',
|
|
||||||
CLEAR = 'clear'
|
|
||||||
}
|
|
|
@ -12,14 +12,7 @@ import {
|
||||||
shallowReadonlyCollectionHandlers
|
shallowReadonlyCollectionHandlers
|
||||||
} from './collectionHandlers'
|
} from './collectionHandlers'
|
||||||
import type { UnwrapRefSimple, Ref, RawSymbol } from './ref'
|
import type { UnwrapRefSimple, Ref, RawSymbol } from './ref'
|
||||||
|
import { ReactiveFlags } from './constants'
|
||||||
export const enum ReactiveFlags {
|
|
||||||
SKIP = '__v_skip',
|
|
||||||
IS_REACTIVE = '__v_isReactive',
|
|
||||||
IS_READONLY = '__v_isReadonly',
|
|
||||||
IS_SHALLOW = '__v_isShallow',
|
|
||||||
RAW = '__v_raw'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Target {
|
export interface Target {
|
||||||
[ReactiveFlags.SKIP]?: boolean
|
[ReactiveFlags.SKIP]?: boolean
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
import { isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
|
||||||
|
import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants'
|
||||||
|
import { createDep, Dep } 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 Sets 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)
|
||||||
|
}
|
|
@ -1,11 +1,10 @@
|
||||||
import {
|
import {
|
||||||
activeEffect,
|
activeEffect,
|
||||||
getDepFromReactive,
|
|
||||||
shouldTrack,
|
shouldTrack,
|
||||||
trackEffects,
|
trackEffect,
|
||||||
triggerEffects
|
triggerEffects
|
||||||
} from './effect'
|
} from './effect'
|
||||||
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants'
|
||||||
import { isArray, hasChanged, IfAny, isFunction, isObject } from '@vue/shared'
|
import { isArray, hasChanged, IfAny, isFunction, isObject } from '@vue/shared'
|
||||||
import {
|
import {
|
||||||
isProxy,
|
isProxy,
|
||||||
|
@ -18,6 +17,8 @@ import {
|
||||||
import type { ShallowReactiveMarker } from './reactive'
|
import type { ShallowReactiveMarker } from './reactive'
|
||||||
import { CollectionTypes } from './collectionHandlers'
|
import { CollectionTypes } from './collectionHandlers'
|
||||||
import { createDep, Dep } from './dep'
|
import { createDep, Dep } from './dep'
|
||||||
|
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
|
||||||
|
@ -40,32 +41,44 @@ type RefBase<T> = {
|
||||||
export function trackRefValue(ref: RefBase<any>) {
|
export function trackRefValue(ref: RefBase<any>) {
|
||||||
if (shouldTrack && activeEffect) {
|
if (shouldTrack && activeEffect) {
|
||||||
ref = toRaw(ref)
|
ref = toRaw(ref)
|
||||||
if (__DEV__) {
|
trackEffect(
|
||||||
trackEffects(ref.dep || (ref.dep = createDep()), {
|
activeEffect,
|
||||||
target: ref,
|
ref.dep ||
|
||||||
type: TrackOpTypes.GET,
|
(ref.dep = createDep(
|
||||||
key: 'value'
|
() => (ref.dep = undefined),
|
||||||
})
|
ref instanceof ComputedRefImpl ? ref : undefined
|
||||||
} else {
|
)),
|
||||||
trackEffects(ref.dep || (ref.dep = createDep()))
|
__DEV__
|
||||||
}
|
? {
|
||||||
|
target: ref,
|
||||||
|
type: TrackOpTypes.GET,
|
||||||
|
key: 'value'
|
||||||
|
}
|
||||||
|
: void 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
|
export function triggerRefValue(
|
||||||
|
ref: RefBase<any>,
|
||||||
|
dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
|
||||||
|
newVal?: any
|
||||||
|
) {
|
||||||
ref = toRaw(ref)
|
ref = toRaw(ref)
|
||||||
const dep = ref.dep
|
const dep = ref.dep
|
||||||
if (dep) {
|
if (dep) {
|
||||||
if (__DEV__) {
|
triggerEffects(
|
||||||
triggerEffects(dep, {
|
dep,
|
||||||
target: ref,
|
dirtyLevel,
|
||||||
type: TriggerOpTypes.SET,
|
__DEV__
|
||||||
key: 'value',
|
? {
|
||||||
newValue: newVal
|
target: ref,
|
||||||
})
|
type: TriggerOpTypes.SET,
|
||||||
} else {
|
key: 'value',
|
||||||
triggerEffects(dep)
|
newValue: newVal
|
||||||
}
|
}
|
||||||
|
: void 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,7 +171,7 @@ class RefImpl<T> {
|
||||||
if (hasChanged(newVal, this._rawValue)) {
|
if (hasChanged(newVal, this._rawValue)) {
|
||||||
this._rawValue = newVal
|
this._rawValue = newVal
|
||||||
this._value = useDirectValue ? newVal : toReactive(newVal)
|
this._value = useDirectValue ? newVal : toReactive(newVal)
|
||||||
triggerRefValue(this, newVal)
|
triggerRefValue(this, DirtyLevels.Dirty, newVal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,7 +202,7 @@ 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, __DEV__ ? ref.value : void 0)
|
triggerRefValue(ref, DirtyLevels.Dirty, __DEV__ ? ref.value : void 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MaybeRef<T = any> = T | Ref<T>
|
export type MaybeRef<T = any> = T | Ref<T>
|
||||||
|
|
|
@ -187,6 +187,7 @@ 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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -322,7 +322,7 @@ function doWatch(
|
||||||
? 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 = () => {
|
||||||
if (!effect.active) {
|
if (!effect.active || !effect.dirty) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (cb) {
|
if (cb) {
|
||||||
|
@ -376,7 +376,7 @@ function doWatch(
|
||||||
scheduler = () => queueJob(job)
|
scheduler = () => queueJob(job)
|
||||||
}
|
}
|
||||||
|
|
||||||
const effect = new ReactiveEffect(getter, scheduler)
|
const effect = new ReactiveEffect(getter, NOOP, scheduler)
|
||||||
|
|
||||||
const unwatch = () => {
|
const unwatch = () => {
|
||||||
effect.stop()
|
effect.stop()
|
||||||
|
|
|
@ -267,7 +267,12 @@ export const publicPropertiesMap: PublicPropertiesMap =
|
||||||
$root: i => getPublicInstance(i.root),
|
$root: i => getPublicInstance(i.root),
|
||||||
$emit: i => i.emit,
|
$emit: i => i.emit,
|
||||||
$options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
|
$options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
|
||||||
$forceUpdate: i => i.f || (i.f = () => queueJob(i.update)),
|
$forceUpdate: i =>
|
||||||
|
i.f ||
|
||||||
|
(i.f = () => {
|
||||||
|
i.effect.dirty = true
|
||||||
|
queueJob(i.update)
|
||||||
|
}),
|
||||||
$nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),
|
$nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),
|
||||||
$watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
|
$watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
|
||||||
} as PublicPropertiesMap)
|
} as PublicPropertiesMap)
|
||||||
|
|
|
@ -246,6 +246,7 @@ const BaseTransitionImpl: ComponentOptions = {
|
||||||
// #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.update.active !== false) {
|
||||||
|
instance.effect.dirty = true
|
||||||
instance.update()
|
instance.update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,6 +93,7 @@ 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
|
||||||
})
|
})
|
||||||
|
@ -137,6 +138,7 @@ 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
|
||||||
|
|
|
@ -1280,6 +1280,7 @@ 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 {
|
||||||
|
@ -1544,11 +1545,16 @@ function baseCreateRenderer(
|
||||||
// create reactive effect for rendering
|
// create reactive effect for rendering
|
||||||
const effect = (instance.effect = new ReactiveEffect(
|
const effect = (instance.effect = new ReactiveEffect(
|
||||||
componentUpdateFn,
|
componentUpdateFn,
|
||||||
|
NOOP,
|
||||||
() => queueJob(update),
|
() => queueJob(update),
|
||||||
instance.scope // track it in component's effect scope
|
instance.scope // track it in component's effect scope
|
||||||
))
|
))
|
||||||
|
|
||||||
const update: SchedulerJob = (instance.update = () => effect.run())
|
const update: SchedulerJob = (instance.update = () => {
|
||||||
|
if (effect.dirty) {
|
||||||
|
effect.run()
|
||||||
|
}
|
||||||
|
})
|
||||||
update.id = instance.uid
|
update.id = instance.uid
|
||||||
// allowRecurse
|
// allowRecurse
|
||||||
// #1801, #2043 component render effects should allow recursive updates
|
// #1801, #2043 component render effects should allow recursive updates
|
||||||
|
|
Loading…
Reference in New Issue