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
|
||||
n.value++
|
||||
// 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', () => {
|
||||
|
@ -288,4 +288,167 @@ describe('reactivity/computed', () => {
|
|||
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', () => {
|
||||
const tick = Promise.resolve()
|
||||
|
||||
test('should only trigger once on multiple mutations', async () => {
|
||||
test('should not trigger if value did not change', () => {
|
||||
const src = ref(0)
|
||||
const c = deferredComputed(() => src.value)
|
||||
const c = computed(() => src.value % 2)
|
||||
const spy = vi.fn()
|
||||
effect(() => {
|
||||
spy(c.value)
|
||||
})
|
||||
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
|
||||
|
||||
await tick
|
||||
// should not trigger
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
src.value = 3
|
||||
src.value = 4
|
||||
src.value = 5
|
||||
await tick
|
||||
// should trigger because latest value changes
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('chained computed trigger', async () => {
|
||||
test('chained computed trigger', () => {
|
||||
const effectSpy = vi.fn()
|
||||
const c1Spy = vi.fn()
|
||||
const c2Spy = vi.fn()
|
||||
|
||||
const src = ref(0)
|
||||
const c1 = deferredComputed(() => {
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
|
@ -69,19 +44,18 @@ describe('deferred computed', () => {
|
|||
expect(effectSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
src.value = 1
|
||||
await tick
|
||||
expect(c1Spy).toHaveBeenCalledTimes(2)
|
||||
expect(c2Spy).toHaveBeenCalledTimes(2)
|
||||
expect(effectSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test('chained computed avoid re-compute', async () => {
|
||||
test('chained computed avoid re-compute', () => {
|
||||
const effectSpy = vi.fn()
|
||||
const c1Spy = vi.fn()
|
||||
const c2Spy = vi.fn()
|
||||
|
||||
const src = ref(0)
|
||||
const c1 = deferredComputed(() => {
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
|
@ -98,26 +72,24 @@ describe('deferred computed', () => {
|
|||
src.value = 2
|
||||
src.value = 4
|
||||
src.value = 6
|
||||
await tick
|
||||
// c1 should re-compute once.
|
||||
expect(c1Spy).toHaveBeenCalledTimes(2)
|
||||
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', async () => {
|
||||
test('chained computed value invalidation', () => {
|
||||
const effectSpy = vi.fn()
|
||||
const c1Spy = vi.fn()
|
||||
const c2Spy = vi.fn()
|
||||
|
||||
const src = ref(0)
|
||||
const c1 = deferredComputed(() => {
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
const c2 = deferredComputed(() => {
|
||||
const c2 = computed(() => {
|
||||
c2Spy()
|
||||
return c1.value + 1
|
||||
})
|
||||
|
@ -139,17 +111,17 @@ describe('deferred computed', () => {
|
|||
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 c1Spy = vi.fn()
|
||||
const c2Spy = vi.fn()
|
||||
|
||||
const src = ref(0)
|
||||
const c1 = deferredComputed(() => {
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
const c2 = deferredComputed(() => {
|
||||
const c2 = computed(() => {
|
||||
c2Spy()
|
||||
return c1.value + 1
|
||||
})
|
||||
|
@ -162,14 +134,13 @@ describe('deferred computed', () => {
|
|||
src.value = 1
|
||||
// sync access c2
|
||||
c2.value
|
||||
await tick
|
||||
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 src = ref(0)
|
||||
const c1 = deferredComputed(() => {
|
||||
const c1 = computed(() => {
|
||||
c1Spy()
|
||||
return src.value % 2
|
||||
})
|
||||
|
@ -179,7 +150,6 @@ describe('deferred computed', () => {
|
|||
c1.effect.stop()
|
||||
// trigger
|
||||
src.value++
|
||||
await tick
|
||||
expect(c1Spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
ref,
|
||||
reactive,
|
||||
effect,
|
||||
stop,
|
||||
|
@ -12,7 +11,8 @@ import {
|
|||
readonly,
|
||||
ReactiveEffectRunner
|
||||
} 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', () => {
|
||||
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(fx1Spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Invoked twice due to change of fx1.
|
||||
expect(fx2Spy).toHaveBeenCalledTimes(2)
|
||||
// Invoked due to change of fx1.
|
||||
expect(fx2Spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
fx1Spy.mockClear()
|
||||
fx2Spy.mockClear()
|
||||
|
@ -821,26 +821,6 @@ describe('reactivity/effect', () => {
|
|||
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', () => {
|
||||
const onStop = vi.fn()
|
||||
const runner = effect(() => {}, {
|
||||
|
@ -1015,4 +995,83 @@ describe('reactivity/effect', () => {
|
|||
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)
|
||||
})
|
||||
|
||||
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', () => {
|
||||
const array = new Array(3)
|
||||
const observed = reactive(array)
|
||||
|
|
|
@ -2,7 +2,6 @@ import {
|
|||
reactive,
|
||||
readonly,
|
||||
toRaw,
|
||||
ReactiveFlags,
|
||||
Target,
|
||||
readonlyMap,
|
||||
reactiveMap,
|
||||
|
@ -11,14 +10,14 @@ import {
|
|||
isReadonly,
|
||||
isShallow
|
||||
} from './reactive'
|
||||
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
||||
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
|
||||
import {
|
||||
track,
|
||||
trigger,
|
||||
ITERATE_KEY,
|
||||
pauseTracking,
|
||||
resetTracking
|
||||
resetTracking,
|
||||
pauseScheduling,
|
||||
resetScheduling
|
||||
} from './effect'
|
||||
import { track, trigger, ITERATE_KEY } from './reactiveEffect'
|
||||
import {
|
||||
isObject,
|
||||
hasOwn,
|
||||
|
@ -71,7 +70,9 @@ function createArrayInstrumentations() {
|
|||
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
|
||||
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
|
||||
pauseTracking()
|
||||
pauseScheduling()
|
||||
const res = (toRaw(this) as any)[key].apply(this, args)
|
||||
resetScheduling()
|
||||
resetTracking()
|
||||
return res
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { toRaw, ReactiveFlags, toReactive, toReadonly } from './reactive'
|
||||
import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
|
||||
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
||||
import { toRaw, toReactive, toReadonly } from './reactive'
|
||||
import {
|
||||
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'
|
||||
|
||||
export type CollectionTypes = IterableCollections | WeakCollections
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { DebuggerOptions, ReactiveEffect } from './effect'
|
||||
import { Ref, trackRefValue, triggerRefValue } from './ref'
|
||||
import { isFunction, NOOP } from '@vue/shared'
|
||||
import { ReactiveFlags, toRaw } from './reactive'
|
||||
import { hasChanged, isFunction, NOOP } from '@vue/shared'
|
||||
import { toRaw } from './reactive'
|
||||
import { Dep } from './dep'
|
||||
import { DirtyLevels, ReactiveFlags } from './constants'
|
||||
|
||||
declare const ComputedRefSymbol: unique symbol
|
||||
|
||||
|
@ -32,7 +33,6 @@ export class ComputedRefImpl<T> {
|
|||
public readonly __v_isRef = true
|
||||
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
|
||||
|
||||
public _dirty = true
|
||||
public _cacheable: boolean
|
||||
|
||||
constructor(
|
||||
|
@ -42,10 +42,7 @@ export class ComputedRefImpl<T> {
|
|||
isSSR: boolean
|
||||
) {
|
||||
this.effect = new ReactiveEffect(getter, () => {
|
||||
if (!this._dirty) {
|
||||
this._dirty = true
|
||||
triggerRefValue(this)
|
||||
}
|
||||
triggerRefValue(this, DirtyLevels.ComputedValueMaybeDirty)
|
||||
})
|
||||
this.effect.computed = this
|
||||
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
|
||||
const self = toRaw(this)
|
||||
trackRefValue(self)
|
||||
if (self._dirty || !self._cacheable) {
|
||||
self._dirty = false
|
||||
self._value = self.effect.run()!
|
||||
if (!self._cacheable || self.effect.dirty) {
|
||||
if (hasChanged(self._value, (self._value = self.effect.run()!))) {
|
||||
triggerRefValue(self, DirtyLevels.ComputedValueDirty)
|
||||
}
|
||||
}
|
||||
return self._value
|
||||
}
|
||||
|
@ -66,6 +64,16 @@ export class ComputedRefImpl<T> {
|
|||
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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 { ReactiveEffect } from './effect'
|
||||
import { ComputedGetter, ComputedRef } from './computed'
|
||||
import { ReactiveFlags, toRaw } from './reactive'
|
||||
import { trackRefValue, triggerRefValue } from './ref'
|
||||
import { computed } from './computed'
|
||||
|
||||
const tick = /*#__PURE__*/ Promise.resolve()
|
||||
const queue: any[] = []
|
||||
let queued = false
|
||||
|
||||
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
|
||||
}
|
||||
/**
|
||||
* @deprecated use `computed` instead. See #5912
|
||||
*/
|
||||
export const deferredComputed = computed
|
||||
|
|
|
@ -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
|
||||
|
||||
/**
|
||||
* 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 type Dep = Map<ReactiveEffect, number> & {
|
||||
cleanup: () => void
|
||||
computed?: ComputedRefImpl<any>
|
||||
}
|
||||
|
||||
export const createDep = (effects?: ReactiveEffect[]): Dep => {
|
||||
const dep = new Set<ReactiveEffect>(effects) as Dep
|
||||
dep.w = 0
|
||||
dep.n = 0
|
||||
export const createDep = (
|
||||
cleanup: () => void,
|
||||
computed?: ComputedRefImpl<any>
|
||||
): Dep => {
|
||||
const dep = new Map() as Dep
|
||||
dep.cleanup = cleanup
|
||||
dep.computed = computed
|
||||
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 { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
|
||||
import { NOOP, extend } 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 {
|
||||
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
|
||||
|
||||
|
@ -47,13 +21,9 @@ export type DebuggerEventExtraInfo = {
|
|||
|
||||
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> {
|
||||
active = true
|
||||
deps: Dep[] = []
|
||||
parent: ReactiveEffect | undefined = undefined
|
||||
|
||||
/**
|
||||
* Can be attached after creation
|
||||
|
@ -64,10 +34,6 @@ export class ReactiveEffect<T = any> {
|
|||
* @internal
|
||||
*/
|
||||
allowRecurse?: boolean
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private deferStop?: boolean
|
||||
|
||||
onStop?: () => void
|
||||
// dev only
|
||||
|
@ -75,77 +41,115 @@ export class ReactiveEffect<T = any> {
|
|||
// dev only
|
||||
onTrigger?: (event: DebuggerEvent) => void
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_dirtyLevel = DirtyLevels.Dirty
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_trackId = 0
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_runnings = 0
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_queryings = 0
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_depsLength = 0
|
||||
|
||||
constructor(
|
||||
public fn: () => T,
|
||||
public scheduler: EffectScheduler | null = null,
|
||||
public trigger: () => void,
|
||||
public scheduler?: EffectScheduler,
|
||||
scope?: EffectScope
|
||||
) {
|
||||
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() {
|
||||
this._dirtyLevel = DirtyLevels.NotDirty
|
||||
if (!this.active) {
|
||||
return this.fn()
|
||||
}
|
||||
let parent: ReactiveEffect | undefined = activeEffect
|
||||
let lastShouldTrack = shouldTrack
|
||||
while (parent) {
|
||||
if (parent === this) {
|
||||
return
|
||||
}
|
||||
parent = parent.parent
|
||||
}
|
||||
let lastEffect = activeEffect
|
||||
try {
|
||||
this.parent = activeEffect
|
||||
activeEffect = this
|
||||
shouldTrack = true
|
||||
|
||||
trackOpBit = 1 << ++effectTrackDepth
|
||||
|
||||
if (effectTrackDepth <= maxMarkerBits) {
|
||||
initDepMarkers(this)
|
||||
} else {
|
||||
cleanupEffect(this)
|
||||
}
|
||||
activeEffect = this
|
||||
this._runnings++
|
||||
preCleanupEffect(this)
|
||||
return this.fn()
|
||||
} finally {
|
||||
if (effectTrackDepth <= maxMarkerBits) {
|
||||
finalizeDepMarkers(this)
|
||||
}
|
||||
|
||||
trackOpBit = 1 << --effectTrackDepth
|
||||
|
||||
activeEffect = this.parent
|
||||
postCleanupEffect(this)
|
||||
this._runnings--
|
||||
activeEffect = lastEffect
|
||||
shouldTrack = lastShouldTrack
|
||||
this.parent = undefined
|
||||
|
||||
if (this.deferStop) {
|
||||
this.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
// stopped while running itself - defer the cleanup
|
||||
if (activeEffect === this) {
|
||||
this.deferStop = true
|
||||
} else if (this.active) {
|
||||
cleanupEffect(this)
|
||||
if (this.onStop) {
|
||||
this.onStop()
|
||||
}
|
||||
if (this.active) {
|
||||
preCleanupEffect(this)
|
||||
postCleanupEffect(this)
|
||||
this.onStop?.()
|
||||
this.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupEffect(effect: ReactiveEffect) {
|
||||
const { deps } = effect
|
||||
if (deps.length) {
|
||||
for (let i = 0; i < deps.length; i++) {
|
||||
deps[i].delete(effect)
|
||||
function triggerComputed(computed: ComputedRefImpl<any>) {
|
||||
return computed.value
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const _effect = new ReactiveEffect(fn)
|
||||
const _effect = new ReactiveEffect(fn, NOOP, () => {
|
||||
if (_effect.dirty) {
|
||||
_effect.run()
|
||||
}
|
||||
})
|
||||
if (options) {
|
||||
extend(_effect, options)
|
||||
if (options.scope) recordEffectScope(_effect, options.scope)
|
||||
|
@ -208,6 +216,8 @@ export function stop(runner: ReactiveEffectRunner) {
|
|||
}
|
||||
|
||||
export let shouldTrack = true
|
||||
export let pauseScheduleStack = 0
|
||||
|
||||
const trackStack: boolean[] = []
|
||||
|
||||
/**
|
||||
|
@ -234,196 +244,70 @@ export function resetTracking() {
|
|||
shouldTrack = last === undefined ? true : last
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()))
|
||||
}
|
||||
export function pauseScheduling() {
|
||||
pauseScheduleStack++
|
||||
}
|
||||
|
||||
const eventInfo = __DEV__
|
||||
? { effect: activeEffect, target, type, key }
|
||||
: undefined
|
||||
|
||||
trackEffects(dep, eventInfo)
|
||||
export function resetScheduling() {
|
||||
pauseScheduleStack--
|
||||
while (!pauseScheduleStack && queueEffectSchedulers.length) {
|
||||
queueEffectSchedulers.shift()!()
|
||||
}
|
||||
}
|
||||
|
||||
export function trackEffects(
|
||||
export function trackEffect(
|
||||
effect: ReactiveEffect,
|
||||
dep: Dep,
|
||||
debuggerEventExtraInfo?: DebuggerEventExtraInfo
|
||||
) {
|
||||
let shouldTrack = false
|
||||
if (effectTrackDepth <= maxMarkerBits) {
|
||||
if (!newTracked(dep)) {
|
||||
dep.n |= trackOpBit // set newly tracked
|
||||
shouldTrack = !wasTracked(dep)
|
||||
}
|
||||
} 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)
|
||||
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__) {
|
||||
triggerEffects(createDep(effects), eventInfo)
|
||||
} else {
|
||||
triggerEffects(createDep(effects))
|
||||
effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const queueEffectSchedulers: (() => void)[] = []
|
||||
|
||||
export function triggerEffects(
|
||||
dep: Dep | ReactiveEffect[],
|
||||
dep: Dep,
|
||||
dirtyLevel: DirtyLevels,
|
||||
debuggerEventExtraInfo?: DebuggerEventExtraInfo
|
||||
) {
|
||||
// spread into array for stabilization
|
||||
const effects = isArray(dep) ? dep : [...dep]
|
||||
for (const effect of effects) {
|
||||
if (effect.computed) {
|
||||
triggerEffect(effect, debuggerEventExtraInfo)
|
||||
}
|
||||
}
|
||||
for (const effect of effects) {
|
||||
if (!effect.computed) {
|
||||
triggerEffect(effect, debuggerEventExtraInfo)
|
||||
pauseScheduling()
|
||||
for (const effect of dep.keys()) {
|
||||
if (!effect.allowRecurse && effect._runnings) {
|
||||
continue
|
||||
}
|
||||
if (
|
||||
effect._dirtyLevel < dirtyLevel &&
|
||||
(!effect._runnings || dirtyLevel !== DirtyLevels.ComputedValueDirty)
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
resetScheduling()
|
||||
}
|
||||
|
|
|
@ -31,7 +31,6 @@ export {
|
|||
shallowReadonly,
|
||||
markRaw,
|
||||
toRaw,
|
||||
ReactiveFlags /* @remove */,
|
||||
type Raw,
|
||||
type DeepReadonly,
|
||||
type ShallowReactive,
|
||||
|
@ -49,12 +48,11 @@ export { deferredComputed } from './deferredComputed'
|
|||
export {
|
||||
effect,
|
||||
stop,
|
||||
trigger,
|
||||
track,
|
||||
enableTracking,
|
||||
pauseTracking,
|
||||
resetTracking,
|
||||
ITERATE_KEY,
|
||||
pauseScheduling,
|
||||
resetScheduling,
|
||||
ReactiveEffect,
|
||||
type ReactiveEffectRunner,
|
||||
type ReactiveEffectOptions,
|
||||
|
@ -63,6 +61,7 @@ export {
|
|||
type DebuggerEvent,
|
||||
type DebuggerEventExtraInfo
|
||||
} from './effect'
|
||||
export { trigger, track, ITERATE_KEY } from './reactiveEffect'
|
||||
export {
|
||||
effectScope,
|
||||
EffectScope,
|
||||
|
@ -71,5 +70,6 @@ export {
|
|||
} from './effectScope'
|
||||
export {
|
||||
TrackOpTypes /* @remove */,
|
||||
TriggerOpTypes /* @remove */
|
||||
} from './operations'
|
||||
TriggerOpTypes /* @remove */,
|
||||
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
|
||||
} from './collectionHandlers'
|
||||
import type { UnwrapRefSimple, Ref, RawSymbol } from './ref'
|
||||
|
||||
export const enum ReactiveFlags {
|
||||
SKIP = '__v_skip',
|
||||
IS_REACTIVE = '__v_isReactive',
|
||||
IS_READONLY = '__v_isReadonly',
|
||||
IS_SHALLOW = '__v_isShallow',
|
||||
RAW = '__v_raw'
|
||||
}
|
||||
import { ReactiveFlags } from './constants'
|
||||
|
||||
export interface Target {
|
||||
[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 {
|
||||
activeEffect,
|
||||
getDepFromReactive,
|
||||
shouldTrack,
|
||||
trackEffects,
|
||||
trackEffect,
|
||||
triggerEffects
|
||||
} from './effect'
|
||||
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
||||
import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants'
|
||||
import { isArray, hasChanged, IfAny, isFunction, isObject } from '@vue/shared'
|
||||
import {
|
||||
isProxy,
|
||||
|
@ -18,6 +17,8 @@ import {
|
|||
import type { ShallowReactiveMarker } from './reactive'
|
||||
import { CollectionTypes } from './collectionHandlers'
|
||||
import { createDep, Dep } from './dep'
|
||||
import { ComputedRefImpl } from './computed'
|
||||
import { getDepFromReactive } from './reactiveEffect'
|
||||
|
||||
declare const RefSymbol: unique symbol
|
||||
export declare const RawSymbol: unique symbol
|
||||
|
@ -40,32 +41,44 @@ type RefBase<T> = {
|
|||
export function trackRefValue(ref: RefBase<any>) {
|
||||
if (shouldTrack && activeEffect) {
|
||||
ref = toRaw(ref)
|
||||
if (__DEV__) {
|
||||
trackEffects(ref.dep || (ref.dep = createDep()), {
|
||||
target: ref,
|
||||
type: TrackOpTypes.GET,
|
||||
key: 'value'
|
||||
})
|
||||
} else {
|
||||
trackEffects(ref.dep || (ref.dep = createDep()))
|
||||
}
|
||||
trackEffect(
|
||||
activeEffect,
|
||||
ref.dep ||
|
||||
(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>, newVal?: any) {
|
||||
export function triggerRefValue(
|
||||
ref: RefBase<any>,
|
||||
dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
|
||||
newVal?: any
|
||||
) {
|
||||
ref = toRaw(ref)
|
||||
const dep = ref.dep
|
||||
if (dep) {
|
||||
if (__DEV__) {
|
||||
triggerEffects(dep, {
|
||||
target: ref,
|
||||
type: TriggerOpTypes.SET,
|
||||
key: 'value',
|
||||
newValue: newVal
|
||||
})
|
||||
} else {
|
||||
triggerEffects(dep)
|
||||
}
|
||||
triggerEffects(
|
||||
dep,
|
||||
dirtyLevel,
|
||||
__DEV__
|
||||
? {
|
||||
target: ref,
|
||||
type: TriggerOpTypes.SET,
|
||||
key: 'value',
|
||||
newValue: newVal
|
||||
}
|
||||
: void 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,7 +171,7 @@ class RefImpl<T> {
|
|||
if (hasChanged(newVal, this._rawValue)) {
|
||||
this._rawValue = 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}
|
||||
*/
|
||||
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>
|
||||
|
|
|
@ -187,6 +187,7 @@ export function defineAsyncComponent<
|
|||
if (instance.parent && isKeepAlive(instance.parent.vnode)) {
|
||||
// parent is keep-alive, force update so the loaded component's
|
||||
// name is taken into account
|
||||
instance.parent.effect.dirty = true
|
||||
queueJob(instance.parent.update)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -322,7 +322,7 @@ function doWatch(
|
|||
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
|
||||
: INITIAL_WATCHER_VALUE
|
||||
const job: SchedulerJob = () => {
|
||||
if (!effect.active) {
|
||||
if (!effect.active || !effect.dirty) {
|
||||
return
|
||||
}
|
||||
if (cb) {
|
||||
|
@ -376,7 +376,7 @@ function doWatch(
|
|||
scheduler = () => queueJob(job)
|
||||
}
|
||||
|
||||
const effect = new ReactiveEffect(getter, scheduler)
|
||||
const effect = new ReactiveEffect(getter, NOOP, scheduler)
|
||||
|
||||
const unwatch = () => {
|
||||
effect.stop()
|
||||
|
|
|
@ -267,7 +267,12 @@ export const publicPropertiesMap: PublicPropertiesMap =
|
|||
$root: i => getPublicInstance(i.root),
|
||||
$emit: i => i.emit,
|
||||
$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!)),
|
||||
$watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
|
||||
} as PublicPropertiesMap)
|
||||
|
|
|
@ -246,6 +246,7 @@ const BaseTransitionImpl: ComponentOptions = {
|
|||
// #6835
|
||||
// it also needs to be updated when active is undefined
|
||||
if (instance.update.active !== false) {
|
||||
instance.effect.dirty = true
|
||||
instance.update()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,6 +93,7 @@ function rerender(id: string, newRender?: Function) {
|
|||
instance.renderCache = []
|
||||
// this flag forces child components with slot content to update
|
||||
isHmrUpdating = true
|
||||
instance.effect.dirty = true
|
||||
instance.update()
|
||||
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
|
||||
// components to be unmounted and re-mounted. Queue the update so that we
|
||||
// don't end up forcing the same parent to re-render multiple times.
|
||||
instance.parent.effect.dirty = true
|
||||
queueJob(instance.parent.update)
|
||||
} else if (instance.appContext.reload) {
|
||||
// root instance mounted via createApp() has a reload method
|
||||
|
|
|
@ -1280,6 +1280,7 @@ function baseCreateRenderer(
|
|||
// double updating the same child component in the same flush.
|
||||
invalidateJob(instance.update)
|
||||
// instance.update is the reactive effect.
|
||||
instance.effect.dirty = true
|
||||
instance.update()
|
||||
}
|
||||
} else {
|
||||
|
@ -1544,11 +1545,16 @@ function baseCreateRenderer(
|
|||
// create reactive effect for rendering
|
||||
const effect = (instance.effect = new ReactiveEffect(
|
||||
componentUpdateFn,
|
||||
NOOP,
|
||||
() => queueJob(update),
|
||||
instance.scope // track it in component's effect scope
|
||||
))
|
||||
|
||||
const update: SchedulerJob = (instance.update = () => effect.run())
|
||||
const update: SchedulerJob = (instance.update = () => {
|
||||
if (effect.dirty) {
|
||||
effect.run()
|
||||
}
|
||||
})
|
||||
update.id = instance.uid
|
||||
// allowRecurse
|
||||
// #1801, #2043 component render effects should allow recursive updates
|
||||
|
|
Loading…
Reference in New Issue