Skip to content

Commit 16e06ca

Browse files
johnsoncodehksxzz
authored andcommittedOct 27, 2023
feat(reactivity): more efficient reactivity system (#5912)
fix #311, fix #1811, fix #6018, fix #7160, fix #8714, fix #9149, fix #9419, fix #9464
1 parent feb2f2e commit 16e06ca

23 files changed

+810
-542
lines changed
 

‎packages/reactivity/__tests__/computed.spec.ts

+164-1
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ describe('reactivity/computed', () => {
184184
// mutate n
185185
n.value++
186186
// on the 2nd run, plusOne.value should have already updated.
187-
expect(plusOneValues).toMatchObject([1, 2, 2])
187+
expect(plusOneValues).toMatchObject([1, 2])
188188
})
189189

190190
it('should warn if trying to set a readonly computed', () => {
@@ -288,4 +288,167 @@ describe('reactivity/computed', () => {
288288
oldValue: 2
289289
})
290290
})
291+
292+
// https://github.com/vuejs/core/pull/5912#issuecomment-1497596875
293+
it('should query deps dirty sequentially', () => {
294+
const cSpy = vi.fn()
295+
296+
const a = ref<null | { v: number }>({
297+
v: 1
298+
})
299+
const b = computed(() => {
300+
return a.value
301+
})
302+
const c = computed(() => {
303+
cSpy()
304+
return b.value?.v
305+
})
306+
const d = computed(() => {
307+
if (b.value) {
308+
return c.value
309+
}
310+
return 0
311+
})
312+
313+
d.value
314+
a.value!.v = 2
315+
a.value = null
316+
d.value
317+
expect(cSpy).toHaveBeenCalledTimes(1)
318+
})
319+
320+
// https://github.com/vuejs/core/pull/5912#issuecomment-1738257692
321+
it('chained computed dirty reallocation after querying dirty', () => {
322+
let _msg: string | undefined
323+
324+
const items = ref<number[]>()
325+
const isLoaded = computed(() => {
326+
return !!items.value
327+
})
328+
const msg = computed(() => {
329+
if (isLoaded.value) {
330+
return 'The items are loaded'
331+
} else {
332+
return 'The items are not loaded'
333+
}
334+
})
335+
336+
effect(() => {
337+
_msg = msg.value
338+
})
339+
340+
items.value = [1, 2, 3]
341+
items.value = [1, 2, 3]
342+
items.value = undefined
343+
344+
expect(_msg).toBe('The items are not loaded')
345+
})
346+
347+
it('chained computed dirty reallocation after trigger computed getter', () => {
348+
let _msg: string | undefined
349+
350+
const items = ref<number[]>()
351+
const isLoaded = computed(() => {
352+
return !!items.value
353+
})
354+
const msg = computed(() => {
355+
if (isLoaded.value) {
356+
return 'The items are loaded'
357+
} else {
358+
return 'The items are not loaded'
359+
}
360+
})
361+
362+
_msg = msg.value
363+
items.value = [1, 2, 3]
364+
isLoaded.value // <- trigger computed getter
365+
_msg = msg.value
366+
items.value = undefined
367+
_msg = msg.value
368+
369+
expect(_msg).toBe('The items are not loaded')
370+
})
371+
372+
// https://github.com/vuejs/core/pull/5912#issuecomment-1739159832
373+
it('deps order should be consistent with the last time get value', () => {
374+
const cSpy = vi.fn()
375+
376+
const a = ref(0)
377+
const b = computed(() => {
378+
return a.value % 3 !== 0
379+
})
380+
const c = computed(() => {
381+
cSpy()
382+
if (a.value % 3 === 2) {
383+
return 'expensive'
384+
}
385+
return 'cheap'
386+
})
387+
const d = computed(() => {
388+
return a.value % 3 === 2
389+
})
390+
const e = computed(() => {
391+
if (b.value) {
392+
if (d.value) {
393+
return 'Avoiding expensive calculation'
394+
}
395+
}
396+
return c.value
397+
})
398+
399+
e.value
400+
a.value++
401+
e.value
402+
403+
expect(e.effect.deps.length).toBe(3)
404+
expect(e.effect.deps.indexOf((b as any).dep)).toBe(0)
405+
expect(e.effect.deps.indexOf((d as any).dep)).toBe(1)
406+
expect(e.effect.deps.indexOf((c as any).dep)).toBe(2)
407+
expect(cSpy).toHaveBeenCalledTimes(2)
408+
409+
a.value++
410+
e.value
411+
412+
expect(cSpy).toHaveBeenCalledTimes(2)
413+
})
414+
415+
it('should trigger by the second computed that maybe dirty', () => {
416+
const cSpy = vi.fn()
417+
418+
const src1 = ref(0)
419+
const src2 = ref(0)
420+
const c1 = computed(() => src1.value)
421+
const c2 = computed(() => (src1.value % 2) + src2.value)
422+
const c3 = computed(() => {
423+
cSpy()
424+
c1.value
425+
c2.value
426+
})
427+
428+
c3.value
429+
src1.value = 2
430+
c3.value
431+
expect(cSpy).toHaveBeenCalledTimes(2)
432+
src2.value = 1
433+
c3.value
434+
expect(cSpy).toHaveBeenCalledTimes(3)
435+
})
436+
437+
it('should trigger the second effect', () => {
438+
const fnSpy = vi.fn()
439+
const v = ref(1)
440+
const c = computed(() => v.value)
441+
442+
effect(() => {
443+
c.value
444+
})
445+
effect(() => {
446+
c.value
447+
fnSpy()
448+
})
449+
450+
expect(fnSpy).toBeCalledTimes(1)
451+
v.value = 2
452+
expect(fnSpy).toBeCalledTimes(2)
453+
})
291454
})
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,32 @@
1-
import { computed, deferredComputed, effect, ref } from '../src'
1+
import { computed, effect, ref } from '../src'
22

33
describe('deferred computed', () => {
4-
const tick = Promise.resolve()
5-
6-
test('should only trigger once on multiple mutations', async () => {
4+
test('should not trigger if value did not change', () => {
75
const src = ref(0)
8-
const c = deferredComputed(() => src.value)
6+
const c = computed(() => src.value % 2)
97
const spy = vi.fn()
108
effect(() => {
119
spy(c.value)
1210
})
1311
expect(spy).toHaveBeenCalledTimes(1)
14-
src.value = 1
1512
src.value = 2
16-
src.value = 3
17-
// not called yet
18-
expect(spy).toHaveBeenCalledTimes(1)
19-
await tick
20-
// should only trigger once
21-
expect(spy).toHaveBeenCalledTimes(2)
22-
expect(spy).toHaveBeenCalledWith(c.value)
23-
})
2413

25-
test('should not trigger if value did not change', async () => {
26-
const src = ref(0)
27-
const c = deferredComputed(() => src.value % 2)
28-
const spy = vi.fn()
29-
effect(() => {
30-
spy(c.value)
31-
})
32-
expect(spy).toHaveBeenCalledTimes(1)
33-
src.value = 1
34-
src.value = 2
35-
36-
await tick
3714
// should not trigger
3815
expect(spy).toHaveBeenCalledTimes(1)
3916

4017
src.value = 3
41-
src.value = 4
4218
src.value = 5
43-
await tick
4419
// should trigger because latest value changes
4520
expect(spy).toHaveBeenCalledTimes(2)
4621
})
4722

48-
test('chained computed trigger', async () => {
23+
test('chained computed trigger', () => {
4924
const effectSpy = vi.fn()
5025
const c1Spy = vi.fn()
5126
const c2Spy = vi.fn()
5227

5328
const src = ref(0)
54-
const c1 = deferredComputed(() => {
29+
const c1 = computed(() => {
5530
c1Spy()
5631
return src.value % 2
5732
})
@@ -69,19 +44,18 @@ describe('deferred computed', () => {
6944
expect(effectSpy).toHaveBeenCalledTimes(1)
7045

7146
src.value = 1
72-
await tick
7347
expect(c1Spy).toHaveBeenCalledTimes(2)
7448
expect(c2Spy).toHaveBeenCalledTimes(2)
7549
expect(effectSpy).toHaveBeenCalledTimes(2)
7650
})
7751

78-
test('chained computed avoid re-compute', async () => {
52+
test('chained computed avoid re-compute', () => {
7953
const effectSpy = vi.fn()
8054
const c1Spy = vi.fn()
8155
const c2Spy = vi.fn()
8256

8357
const src = ref(0)
84-
const c1 = deferredComputed(() => {
58+
const c1 = computed(() => {
8559
c1Spy()
8660
return src.value % 2
8761
})
@@ -98,26 +72,24 @@ describe('deferred computed', () => {
9872
src.value = 2
9973
src.value = 4
10074
src.value = 6
101-
await tick
102-
// c1 should re-compute once.
103-
expect(c1Spy).toHaveBeenCalledTimes(2)
75+
expect(c1Spy).toHaveBeenCalledTimes(4)
10476
// c2 should not have to re-compute because c1 did not change.
10577
expect(c2Spy).toHaveBeenCalledTimes(1)
10678
// effect should not trigger because c2 did not change.
10779
expect(effectSpy).toHaveBeenCalledTimes(1)
10880
})
10981

110-
test('chained computed value invalidation', async () => {
82+
test('chained computed value invalidation', () => {
11183
const effectSpy = vi.fn()
11284
const c1Spy = vi.fn()
11385
const c2Spy = vi.fn()
11486

11587
const src = ref(0)
116-
const c1 = deferredComputed(() => {
88+
const c1 = computed(() => {
11789
c1Spy()
11890
return src.value % 2
11991
})
120-
const c2 = deferredComputed(() => {
92+
const c2 = computed(() => {
12193
c2Spy()
12294
return c1.value + 1
12395
})
@@ -139,17 +111,17 @@ describe('deferred computed', () => {
139111
expect(c2Spy).toHaveBeenCalledTimes(2)
140112
})
141113

142-
test('sync access of invalidated chained computed should not prevent final effect from running', async () => {
114+
test('sync access of invalidated chained computed should not prevent final effect from running', () => {
143115
const effectSpy = vi.fn()
144116
const c1Spy = vi.fn()
145117
const c2Spy = vi.fn()
146118

147119
const src = ref(0)
148-
const c1 = deferredComputed(() => {
120+
const c1 = computed(() => {
149121
c1Spy()
150122
return src.value % 2
151123
})
152-
const c2 = deferredComputed(() => {
124+
const c2 = computed(() => {
153125
c2Spy()
154126
return c1.value + 1
155127
})
@@ -162,14 +134,13 @@ describe('deferred computed', () => {
162134
src.value = 1
163135
// sync access c2
164136
c2.value
165-
await tick
166137
expect(effectSpy).toHaveBeenCalledTimes(2)
167138
})
168139

169-
test('should not compute if deactivated before scheduler is called', async () => {
140+
test('should not compute if deactivated before scheduler is called', () => {
170141
const c1Spy = vi.fn()
171142
const src = ref(0)
172-
const c1 = deferredComputed(() => {
143+
const c1 = computed(() => {
173144
c1Spy()
174145
return src.value % 2
175146
})
@@ -179,7 +150,6 @@ describe('deferred computed', () => {
179150
c1.effect.stop()
180151
// trigger
181152
src.value++
182-
await tick
183153
expect(c1Spy).toHaveBeenCalledTimes(1)
184154
})
185155
})

‎packages/reactivity/__tests__/effect.spec.ts

+83-24
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
ref,
32
reactive,
43
effect,
54
stop,
@@ -12,7 +11,8 @@ import {
1211
readonly,
1312
ReactiveEffectRunner
1413
} from '../src/index'
15-
import { ITERATE_KEY } from '../src/effect'
14+
import { pauseScheduling, resetScheduling } from '../src/effect'
15+
import { ITERATE_KEY, getDepFromReactive } from '../src/reactiveEffect'
1616

1717
describe('reactivity/effect', () => {
1818
it('should run the passed function once (wrapped by a effect)', () => {
@@ -574,8 +574,8 @@ describe('reactivity/effect', () => {
574574
expect(output.fx2).toBe(1 + 3 + 3)
575575
expect(fx1Spy).toHaveBeenCalledTimes(1)
576576

577-
// Invoked twice due to change of fx1.
578-
expect(fx2Spy).toHaveBeenCalledTimes(2)
577+
// Invoked due to change of fx1.
578+
expect(fx2Spy).toHaveBeenCalledTimes(1)
579579

580580
fx1Spy.mockClear()
581581
fx2Spy.mockClear()
@@ -821,26 +821,6 @@ describe('reactivity/effect', () => {
821821
expect(dummy).toBe(3)
822822
})
823823

824-
// #5707
825-
// when an effect completes its run, it should clear the tracking bits of
826-
// its tracked deps. However, if the effect stops itself, the deps list is
827-
// emptied so their bits are never cleared.
828-
it('edge case: self-stopping effect tracking ref', () => {
829-
const c = ref(true)
830-
const runner = effect(() => {
831-
// reference ref
832-
if (!c.value) {
833-
// stop itself while running
834-
stop(runner)
835-
}
836-
})
837-
// trigger run
838-
c.value = !c.value
839-
// should clear bits
840-
expect((c as any).dep.w).toBe(0)
841-
expect((c as any).dep.n).toBe(0)
842-
})
843-
844824
it('events: onStop', () => {
845825
const onStop = vi.fn()
846826
const runner = effect(() => {}, {
@@ -1015,4 +995,83 @@ describe('reactivity/effect', () => {
1015995
expect(has).toBe(false)
1016996
})
1017997
})
998+
999+
it('should be triggered once with pauseScheduling', () => {
1000+
const counter = reactive({ num: 0 })
1001+
1002+
const counterSpy = vi.fn(() => counter.num)
1003+
effect(counterSpy)
1004+
1005+
counterSpy.mockClear()
1006+
1007+
pauseScheduling()
1008+
counter.num++
1009+
counter.num++
1010+
resetScheduling()
1011+
expect(counterSpy).toHaveBeenCalledTimes(1)
1012+
})
1013+
1014+
describe('empty dep cleanup', () => {
1015+
it('should remove the dep when the effect is stopped', () => {
1016+
const obj = reactive({ prop: 1 })
1017+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
1018+
const runner = effect(() => obj.prop)
1019+
const dep = getDepFromReactive(toRaw(obj), 'prop')
1020+
expect(dep).toHaveLength(1)
1021+
obj.prop = 2
1022+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
1023+
expect(dep).toHaveLength(1)
1024+
stop(runner)
1025+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
1026+
obj.prop = 3
1027+
runner()
1028+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
1029+
})
1030+
1031+
it('should only remove the dep when the last effect is stopped', () => {
1032+
const obj = reactive({ prop: 1 })
1033+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
1034+
const runner1 = effect(() => obj.prop)
1035+
const dep = getDepFromReactive(toRaw(obj), 'prop')
1036+
expect(dep).toHaveLength(1)
1037+
const runner2 = effect(() => obj.prop)
1038+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
1039+
expect(dep).toHaveLength(2)
1040+
obj.prop = 2
1041+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
1042+
expect(dep).toHaveLength(2)
1043+
stop(runner1)
1044+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
1045+
expect(dep).toHaveLength(1)
1046+
obj.prop = 3
1047+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
1048+
expect(dep).toHaveLength(1)
1049+
stop(runner2)
1050+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
1051+
obj.prop = 4
1052+
runner1()
1053+
runner2()
1054+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
1055+
})
1056+
1057+
it('should remove the dep when it is no longer used by the effect', () => {
1058+
const obj = reactive<{ a: number; b: number; c: 'a' | 'b' }>({
1059+
a: 1,
1060+
b: 2,
1061+
c: 'a'
1062+
})
1063+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
1064+
effect(() => obj[obj.c])
1065+
const depC = getDepFromReactive(toRaw(obj), 'c')
1066+
expect(getDepFromReactive(toRaw(obj), 'a')).toHaveLength(1)
1067+
expect(getDepFromReactive(toRaw(obj), 'b')).toBeUndefined()
1068+
expect(depC).toHaveLength(1)
1069+
obj.c = 'b'
1070+
obj.a = 4
1071+
expect(getDepFromReactive(toRaw(obj), 'a')).toBeUndefined()
1072+
expect(getDepFromReactive(toRaw(obj), 'b')).toHaveLength(1)
1073+
expect(getDepFromReactive(toRaw(obj), 'c')).toBe(depC)
1074+
expect(depC).toHaveLength(1)
1075+
})
1076+
})
10181077
})
+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {
2+
ComputedRef,
3+
computed,
4+
effect,
5+
reactive,
6+
shallowRef as ref,
7+
toRaw
8+
} from '../src/index'
9+
import { getDepFromReactive } from '../src/reactiveEffect'
10+
11+
describe.skipIf(!global.gc)('reactivity/gc', () => {
12+
const gc = () => {
13+
return new Promise<void>(resolve => {
14+
setTimeout(() => {
15+
global.gc!()
16+
resolve()
17+
})
18+
})
19+
}
20+
21+
// #9233
22+
it('should release computed cache', async () => {
23+
const src = ref<{} | undefined>({})
24+
const srcRef = new WeakRef(src.value!)
25+
26+
let c: ComputedRef | undefined = computed(() => src.value)
27+
28+
c.value // cache src value
29+
src.value = undefined // release value
30+
c = undefined // release computed
31+
32+
await gc()
33+
expect(srcRef.deref()).toBeUndefined()
34+
})
35+
36+
it('should release reactive property dep', async () => {
37+
const src = reactive({ foo: 1 })
38+
39+
let c: ComputedRef | undefined = computed(() => src.foo)
40+
41+
c.value
42+
expect(getDepFromReactive(toRaw(src), 'foo')).not.toBeUndefined()
43+
44+
c = undefined
45+
await gc()
46+
await gc()
47+
expect(getDepFromReactive(toRaw(src), 'foo')).toBeUndefined()
48+
})
49+
50+
it('should not release effect for ref', async () => {
51+
const spy = vi.fn()
52+
const src = ref(0)
53+
54+
effect(() => {
55+
spy()
56+
src.value
57+
})
58+
59+
expect(spy).toHaveBeenCalledTimes(1)
60+
61+
await gc()
62+
src.value++
63+
expect(spy).toHaveBeenCalledTimes(2)
64+
})
65+
66+
it('should not release effect for reactive', async () => {
67+
const spy = vi.fn()
68+
const src = reactive({ foo: 1 })
69+
70+
effect(() => {
71+
spy()
72+
src.foo
73+
})
74+
75+
expect(spy).toHaveBeenCalledTimes(1)
76+
77+
await gc()
78+
src.foo++
79+
expect(spy).toHaveBeenCalledTimes(2)
80+
})
81+
})

‎packages/reactivity/__tests__/reactiveArray.spec.ts

+33
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,39 @@ describe('reactivity/reactive/Array', () => {
9999
expect(fn).toHaveBeenCalledTimes(1)
100100
})
101101

102+
test('shift on Array should trigger dependency once', () => {
103+
const arr = reactive([1, 2, 3])
104+
const fn = vi.fn()
105+
effect(() => {
106+
for (let i = 0; i < arr.length; i++) {
107+
arr[i]
108+
}
109+
fn()
110+
})
111+
expect(fn).toHaveBeenCalledTimes(1)
112+
arr.shift()
113+
expect(fn).toHaveBeenCalledTimes(2)
114+
})
115+
116+
//#6018
117+
test('edge case: avoid trigger effect in deleteProperty when array length-decrease mutation methods called', () => {
118+
const arr = ref([1])
119+
const fn1 = vi.fn()
120+
const fn2 = vi.fn()
121+
effect(() => {
122+
fn1()
123+
if (arr.value.length > 0) {
124+
arr.value.slice()
125+
fn2()
126+
}
127+
})
128+
expect(fn1).toHaveBeenCalledTimes(1)
129+
expect(fn2).toHaveBeenCalledTimes(1)
130+
arr.value.splice(0)
131+
expect(fn1).toHaveBeenCalledTimes(2)
132+
expect(fn2).toHaveBeenCalledTimes(1)
133+
})
134+
102135
test('add existing index on Array should not trigger length dependency', () => {
103136
const array = new Array(3)
104137
const observed = reactive(array)

‎packages/reactivity/src/baseHandlers.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
reactive,
33
readonly,
44
toRaw,
5-
ReactiveFlags,
65
Target,
76
readonlyMap,
87
reactiveMap,
@@ -11,14 +10,14 @@ import {
1110
isReadonly,
1211
isShallow
1312
} from './reactive'
14-
import { TrackOpTypes, TriggerOpTypes } from './operations'
13+
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
1514
import {
16-
track,
17-
trigger,
18-
ITERATE_KEY,
1915
pauseTracking,
20-
resetTracking
16+
resetTracking,
17+
pauseScheduling,
18+
resetScheduling
2119
} from './effect'
20+
import { track, trigger, ITERATE_KEY } from './reactiveEffect'
2221
import {
2322
isObject,
2423
hasOwn,
@@ -71,7 +70,9 @@ function createArrayInstrumentations() {
7170
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
7271
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
7372
pauseTracking()
73+
pauseScheduling()
7474
const res = (toRaw(this) as any)[key].apply(this, args)
75+
resetScheduling()
7576
resetTracking()
7677
return res
7778
}

‎packages/reactivity/src/collectionHandlers.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import { toRaw, ReactiveFlags, toReactive, toReadonly } from './reactive'
2-
import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
3-
import { TrackOpTypes, TriggerOpTypes } from './operations'
1+
import { toRaw, toReactive, toReadonly } from './reactive'
2+
import {
3+
track,
4+
trigger,
5+
ITERATE_KEY,
6+
MAP_KEY_ITERATE_KEY
7+
} from './reactiveEffect'
8+
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
49
import { capitalize, hasOwn, hasChanged, toRawType, isMap } from '@vue/shared'
510

611
export type CollectionTypes = IterableCollections | WeakCollections

‎packages/reactivity/src/computed.ts

+18-10
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { DebuggerOptions, ReactiveEffect } from './effect'
22
import { Ref, trackRefValue, triggerRefValue } from './ref'
3-
import { isFunction, NOOP } from '@vue/shared'
4-
import { ReactiveFlags, toRaw } from './reactive'
3+
import { hasChanged, isFunction, NOOP } from '@vue/shared'
4+
import { toRaw } from './reactive'
55
import { Dep } from './dep'
6+
import { DirtyLevels, ReactiveFlags } from './constants'
67

78
declare const ComputedRefSymbol: unique symbol
89

@@ -32,7 +33,6 @@ export class ComputedRefImpl<T> {
3233
public readonly __v_isRef = true
3334
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
3435

35-
public _dirty = true
3636
public _cacheable: boolean
3737

3838
constructor(
@@ -42,10 +42,7 @@ export class ComputedRefImpl<T> {
4242
isSSR: boolean
4343
) {
4444
this.effect = new ReactiveEffect(getter, () => {
45-
if (!this._dirty) {
46-
this._dirty = true
47-
triggerRefValue(this)
48-
}
45+
triggerRefValue(this, DirtyLevels.ComputedValueMaybeDirty)
4946
})
5047
this.effect.computed = this
5148
this.effect.active = this._cacheable = !isSSR
@@ -56,16 +53,27 @@ export class ComputedRefImpl<T> {
5653
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
5754
const self = toRaw(this)
5855
trackRefValue(self)
59-
if (self._dirty || !self._cacheable) {
60-
self._dirty = false
61-
self._value = self.effect.run()!
56+
if (!self._cacheable || self.effect.dirty) {
57+
if (hasChanged(self._value, (self._value = self.effect.run()!))) {
58+
triggerRefValue(self, DirtyLevels.ComputedValueDirty)
59+
}
6260
}
6361
return self._value
6462
}
6563

6664
set value(newValue: T) {
6765
this._setter(newValue)
6866
}
67+
68+
// #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x
69+
get _dirty() {
70+
return this.effect.dirty
71+
}
72+
73+
set _dirty(v) {
74+
this.effect.dirty = v
75+
}
76+
// #endregion
6977
}
7078

7179
/**

‎packages/reactivity/src/constants.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// using literal strings instead of numbers so that it's easier to inspect
2+
// debugger events
3+
4+
export const enum TrackOpTypes {
5+
GET = 'get',
6+
HAS = 'has',
7+
ITERATE = 'iterate'
8+
}
9+
10+
export const enum TriggerOpTypes {
11+
SET = 'set',
12+
ADD = 'add',
13+
DELETE = 'delete',
14+
CLEAR = 'clear'
15+
}
16+
17+
export const enum ReactiveFlags {
18+
SKIP = '__v_skip',
19+
IS_REACTIVE = '__v_isReactive',
20+
IS_READONLY = '__v_isReadonly',
21+
IS_SHALLOW = '__v_isShallow',
22+
RAW = '__v_raw'
23+
}
24+
25+
export const enum DirtyLevels {
26+
NotDirty = 0,
27+
ComputedValueMaybeDirty = 1,
28+
ComputedValueDirty = 2,
29+
Dirty = 3
30+
}
+5-87
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,6 @@
1-
import { Dep } from './dep'
2-
import { ReactiveEffect } from './effect'
3-
import { ComputedGetter, ComputedRef } from './computed'
4-
import { ReactiveFlags, toRaw } from './reactive'
5-
import { trackRefValue, triggerRefValue } from './ref'
1+
import { computed } from './computed'
62

7-
const tick = /*#__PURE__*/ Promise.resolve()
8-
const queue: any[] = []
9-
let queued = false
10-
11-
const scheduler = (fn: any) => {
12-
queue.push(fn)
13-
if (!queued) {
14-
queued = true
15-
tick.then(flush)
16-
}
17-
}
18-
19-
const flush = () => {
20-
for (let i = 0; i < queue.length; i++) {
21-
queue[i]()
22-
}
23-
queue.length = 0
24-
queued = false
25-
}
26-
27-
class DeferredComputedRefImpl<T> {
28-
public dep?: Dep = undefined
29-
30-
private _value!: T
31-
private _dirty = true
32-
public readonly effect: ReactiveEffect<T>
33-
34-
public readonly __v_isRef = true
35-
public readonly [ReactiveFlags.IS_READONLY] = true
36-
37-
constructor(getter: ComputedGetter<T>) {
38-
let compareTarget: any
39-
let hasCompareTarget = false
40-
let scheduled = false
41-
this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => {
42-
if (this.dep) {
43-
if (computedTrigger) {
44-
compareTarget = this._value
45-
hasCompareTarget = true
46-
} else if (!scheduled) {
47-
const valueToCompare = hasCompareTarget ? compareTarget : this._value
48-
scheduled = true
49-
hasCompareTarget = false
50-
scheduler(() => {
51-
if (this.effect.active && this._get() !== valueToCompare) {
52-
triggerRefValue(this)
53-
}
54-
scheduled = false
55-
})
56-
}
57-
// chained upstream computeds are notified synchronously to ensure
58-
// value invalidation in case of sync access; normal effects are
59-
// deferred to be triggered in scheduler.
60-
for (const e of this.dep) {
61-
if (e.computed instanceof DeferredComputedRefImpl) {
62-
e.scheduler!(true /* computedTrigger */)
63-
}
64-
}
65-
}
66-
this._dirty = true
67-
})
68-
this.effect.computed = this as any
69-
}
70-
71-
private _get() {
72-
if (this._dirty) {
73-
this._dirty = false
74-
return (this._value = this.effect.run()!)
75-
}
76-
return this._value
77-
}
78-
79-
get value() {
80-
trackRefValue(this)
81-
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
82-
return toRaw(this)._get()
83-
}
84-
}
85-
86-
export function deferredComputed<T>(getter: () => T): ComputedRef<T> {
87-
return new DeferredComputedRefImpl(getter) as any
88-
}
3+
/**
4+
* @deprecated use `computed` instead. See #5912
5+
*/
6+
export const deferredComputed = computed

‎packages/reactivity/src/dep.ts

+12-52
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,17 @@
1-
import { ReactiveEffect, trackOpBit } from './effect'
1+
import type { ReactiveEffect } from './effect'
2+
import type { ComputedRefImpl } from './computed'
23

3-
export type Dep = Set<ReactiveEffect> & TrackedMarkers
4-
5-
/**
6-
* wasTracked and newTracked maintain the status for several levels of effect
7-
* tracking recursion. One bit per level is used to define whether the dependency
8-
* was/is tracked.
9-
*/
10-
type TrackedMarkers = {
11-
/**
12-
* wasTracked
13-
*/
14-
w: number
15-
/**
16-
* newTracked
17-
*/
18-
n: number
4+
export type Dep = Map<ReactiveEffect, number> & {
5+
cleanup: () => void
6+
computed?: ComputedRefImpl<any>
197
}
208

21-
export const createDep = (effects?: ReactiveEffect[]): Dep => {
22-
const dep = new Set<ReactiveEffect>(effects) as Dep
23-
dep.w = 0
24-
dep.n = 0
9+
export const createDep = (
10+
cleanup: () => void,
11+
computed?: ComputedRefImpl<any>
12+
): Dep => {
13+
const dep = new Map() as Dep
14+
dep.cleanup = cleanup
15+
dep.computed = computed
2516
return dep
2617
}
27-
28-
export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
29-
30-
export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0
31-
32-
export const initDepMarkers = ({ deps }: ReactiveEffect) => {
33-
if (deps.length) {
34-
for (let i = 0; i < deps.length; i++) {
35-
deps[i].w |= trackOpBit // set was tracked
36-
}
37-
}
38-
}
39-
40-
export const finalizeDepMarkers = (effect: ReactiveEffect) => {
41-
const { deps } = effect
42-
if (deps.length) {
43-
let ptr = 0
44-
for (let i = 0; i < deps.length; i++) {
45-
const dep = deps[i]
46-
if (wasTracked(dep) && !newTracked(dep)) {
47-
dep.delete(effect)
48-
} else {
49-
deps[ptr++] = dep
50-
}
51-
// clear bits
52-
dep.w &= ~trackOpBit
53-
dep.n &= ~trackOpBit
54-
}
55-
deps.length = ptr
56-
}
57-
}

‎packages/reactivity/src/effect.ts

+139-255
Large diffs are not rendered by default.

‎packages/reactivity/src/index.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ export {
3131
shallowReadonly,
3232
markRaw,
3333
toRaw,
34-
ReactiveFlags /* @remove */,
3534
type Raw,
3635
type DeepReadonly,
3736
type ShallowReactive,
@@ -49,12 +48,11 @@ export { deferredComputed } from './deferredComputed'
4948
export {
5049
effect,
5150
stop,
52-
trigger,
53-
track,
5451
enableTracking,
5552
pauseTracking,
5653
resetTracking,
57-
ITERATE_KEY,
54+
pauseScheduling,
55+
resetScheduling,
5856
ReactiveEffect,
5957
type ReactiveEffectRunner,
6058
type ReactiveEffectOptions,
@@ -63,6 +61,7 @@ export {
6361
type DebuggerEvent,
6462
type DebuggerEventExtraInfo
6563
} from './effect'
64+
export { trigger, track, ITERATE_KEY } from './reactiveEffect'
6665
export {
6766
effectScope,
6867
EffectScope,
@@ -71,5 +70,6 @@ export {
7170
} from './effectScope'
7271
export {
7372
TrackOpTypes /* @remove */,
74-
TriggerOpTypes /* @remove */
75-
} from './operations'
73+
TriggerOpTypes /* @remove */,
74+
ReactiveFlags /* @remove */
75+
} from './constants'

‎packages/reactivity/src/operations.ts

-15
This file was deleted.

‎packages/reactivity/src/reactive.ts

+1-8
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,7 @@ import {
1212
shallowReadonlyCollectionHandlers
1313
} from './collectionHandlers'
1414
import type { UnwrapRefSimple, Ref, RawSymbol } from './ref'
15-
16-
export const enum ReactiveFlags {
17-
SKIP = '__v_skip',
18-
IS_REACTIVE = '__v_isReactive',
19-
IS_READONLY = '__v_isReadonly',
20-
IS_SHALLOW = '__v_isShallow',
21-
RAW = '__v_raw'
22-
}
15+
import { ReactiveFlags } from './constants'
2316

2417
export interface Target {
2518
[ReactiveFlags.SKIP]?: boolean
+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
2+
import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants'
3+
import { createDep, Dep } from './dep'
4+
import {
5+
activeEffect,
6+
pauseScheduling,
7+
resetScheduling,
8+
shouldTrack,
9+
trackEffect,
10+
triggerEffects
11+
} from './effect'
12+
13+
// The main WeakMap that stores {target -> key -> dep} connections.
14+
// Conceptually, it's easier to think of a dependency as a Dep class
15+
// which maintains a Set of subscribers, but we simply store them as
16+
// raw Sets to reduce memory overhead.
17+
type KeyToDepMap = Map<any, Dep>
18+
const targetMap = new WeakMap<object, KeyToDepMap>()
19+
20+
export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
21+
export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '')
22+
23+
/**
24+
* Tracks access to a reactive property.
25+
*
26+
* This will check which effect is running at the moment and record it as dep
27+
* which records all effects that depend on the reactive property.
28+
*
29+
* @param target - Object holding the reactive property.
30+
* @param type - Defines the type of access to the reactive property.
31+
* @param key - Identifier of the reactive property to track.
32+
*/
33+
export function track(target: object, type: TrackOpTypes, key: unknown) {
34+
if (shouldTrack && activeEffect) {
35+
let depsMap = targetMap.get(target)
36+
if (!depsMap) {
37+
targetMap.set(target, (depsMap = new Map()))
38+
}
39+
let dep = depsMap.get(key)
40+
if (!dep) {
41+
depsMap.set(key, (dep = createDep(() => depsMap!.delete(key))))
42+
}
43+
trackEffect(
44+
activeEffect,
45+
dep,
46+
__DEV__
47+
? {
48+
target,
49+
type,
50+
key
51+
}
52+
: void 0
53+
)
54+
}
55+
}
56+
57+
/**
58+
* Finds all deps associated with the target (or a specific property) and
59+
* triggers the effects stored within.
60+
*
61+
* @param target - The reactive object.
62+
* @param type - Defines the type of the operation that needs to trigger effects.
63+
* @param key - Can be used to target a specific reactive property in the target object.
64+
*/
65+
export function trigger(
66+
target: object,
67+
type: TriggerOpTypes,
68+
key?: unknown,
69+
newValue?: unknown,
70+
oldValue?: unknown,
71+
oldTarget?: Map<unknown, unknown> | Set<unknown>
72+
) {
73+
const depsMap = targetMap.get(target)
74+
if (!depsMap) {
75+
// never been tracked
76+
return
77+
}
78+
79+
let deps: (Dep | undefined)[] = []
80+
if (type === TriggerOpTypes.CLEAR) {
81+
// collection being cleared
82+
// trigger all effects for target
83+
deps = [...depsMap.values()]
84+
} else if (key === 'length' && isArray(target)) {
85+
const newLength = Number(newValue)
86+
depsMap.forEach((dep, key) => {
87+
if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
88+
deps.push(dep)
89+
}
90+
})
91+
} else {
92+
// schedule runs for SET | ADD | DELETE
93+
if (key !== void 0) {
94+
deps.push(depsMap.get(key))
95+
}
96+
97+
// also run for iteration key on ADD | DELETE | Map.SET
98+
switch (type) {
99+
case TriggerOpTypes.ADD:
100+
if (!isArray(target)) {
101+
deps.push(depsMap.get(ITERATE_KEY))
102+
if (isMap(target)) {
103+
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
104+
}
105+
} else if (isIntegerKey(key)) {
106+
// new index added to array -> length changes
107+
deps.push(depsMap.get('length'))
108+
}
109+
break
110+
case TriggerOpTypes.DELETE:
111+
if (!isArray(target)) {
112+
deps.push(depsMap.get(ITERATE_KEY))
113+
if (isMap(target)) {
114+
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
115+
}
116+
}
117+
break
118+
case TriggerOpTypes.SET:
119+
if (isMap(target)) {
120+
deps.push(depsMap.get(ITERATE_KEY))
121+
}
122+
break
123+
}
124+
}
125+
126+
pauseScheduling()
127+
for (const dep of deps) {
128+
if (dep) {
129+
triggerEffects(
130+
dep,
131+
DirtyLevels.Dirty,
132+
__DEV__
133+
? {
134+
target,
135+
type,
136+
key,
137+
newValue,
138+
oldValue,
139+
oldTarget
140+
}
141+
: void 0
142+
)
143+
}
144+
}
145+
resetScheduling()
146+
}
147+
148+
export function getDepFromReactive(object: any, key: string | number | symbol) {
149+
return targetMap.get(object)?.get(key)
150+
}

‎packages/reactivity/src/ref.ts

+38-25
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import {
22
activeEffect,
3-
getDepFromReactive,
43
shouldTrack,
5-
trackEffects,
4+
trackEffect,
65
triggerEffects
76
} from './effect'
8-
import { TrackOpTypes, TriggerOpTypes } from './operations'
7+
import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants'
98
import { isArray, hasChanged, IfAny, isFunction, isObject } from '@vue/shared'
109
import {
1110
isProxy,
@@ -18,6 +17,8 @@ import {
1817
import type { ShallowReactiveMarker } from './reactive'
1918
import { CollectionTypes } from './collectionHandlers'
2019
import { createDep, Dep } from './dep'
20+
import { ComputedRefImpl } from './computed'
21+
import { getDepFromReactive } from './reactiveEffect'
2122

2223
declare const RefSymbol: unique symbol
2324
export declare const RawSymbol: unique symbol
@@ -40,32 +41,44 @@ type RefBase<T> = {
4041
export function trackRefValue(ref: RefBase<any>) {
4142
if (shouldTrack && activeEffect) {
4243
ref = toRaw(ref)
43-
if (__DEV__) {
44-
trackEffects(ref.dep || (ref.dep = createDep()), {
45-
target: ref,
46-
type: TrackOpTypes.GET,
47-
key: 'value'
48-
})
49-
} else {
50-
trackEffects(ref.dep || (ref.dep = createDep()))
51-
}
44+
trackEffect(
45+
activeEffect,
46+
ref.dep ||
47+
(ref.dep = createDep(
48+
() => (ref.dep = undefined),
49+
ref instanceof ComputedRefImpl ? ref : undefined
50+
)),
51+
__DEV__
52+
? {
53+
target: ref,
54+
type: TrackOpTypes.GET,
55+
key: 'value'
56+
}
57+
: void 0
58+
)
5259
}
5360
}
5461

55-
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
62+
export function triggerRefValue(
63+
ref: RefBase<any>,
64+
dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
65+
newVal?: any
66+
) {
5667
ref = toRaw(ref)
5768
const dep = ref.dep
5869
if (dep) {
59-
if (__DEV__) {
60-
triggerEffects(dep, {
61-
target: ref,
62-
type: TriggerOpTypes.SET,
63-
key: 'value',
64-
newValue: newVal
65-
})
66-
} else {
67-
triggerEffects(dep)
68-
}
70+
triggerEffects(
71+
dep,
72+
dirtyLevel,
73+
__DEV__
74+
? {
75+
target: ref,
76+
type: TriggerOpTypes.SET,
77+
key: 'value',
78+
newValue: newVal
79+
}
80+
: void 0
81+
)
6982
}
7083
}
7184

@@ -158,7 +171,7 @@ class RefImpl<T> {
158171
if (hasChanged(newVal, this._rawValue)) {
159172
this._rawValue = newVal
160173
this._value = useDirectValue ? newVal : toReactive(newVal)
161-
triggerRefValue(this, newVal)
174+
triggerRefValue(this, DirtyLevels.Dirty, newVal)
162175
}
163176
}
164177
}
@@ -189,7 +202,7 @@ class RefImpl<T> {
189202
* @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref}
190203
*/
191204
export function triggerRef(ref: Ref) {
192-
triggerRefValue(ref, __DEV__ ? ref.value : void 0)
205+
triggerRefValue(ref, DirtyLevels.Dirty, __DEV__ ? ref.value : void 0)
193206
}
194207

195208
export type MaybeRef<T = any> = T | Ref<T>

‎packages/runtime-core/src/apiAsyncComponent.ts

+1
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ export function defineAsyncComponent<
187187
if (instance.parent && isKeepAlive(instance.parent.vnode)) {
188188
// parent is keep-alive, force update so the loaded component's
189189
// name is taken into account
190+
instance.parent.effect.dirty = true
190191
queueJob(instance.parent.update)
191192
}
192193
})

‎packages/runtime-core/src/apiWatch.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ function doWatch(
322322
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
323323
: INITIAL_WATCHER_VALUE
324324
const job: SchedulerJob = () => {
325-
if (!effect.active) {
325+
if (!effect.active || !effect.dirty) {
326326
return
327327
}
328328
if (cb) {
@@ -376,7 +376,7 @@ function doWatch(
376376
scheduler = () => queueJob(job)
377377
}
378378

379-
const effect = new ReactiveEffect(getter, scheduler)
379+
const effect = new ReactiveEffect(getter, NOOP, scheduler)
380380

381381
const unwatch = () => {
382382
effect.stop()

‎packages/runtime-core/src/componentPublicInstance.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,12 @@ export const publicPropertiesMap: PublicPropertiesMap =
267267
$root: i => getPublicInstance(i.root),
268268
$emit: i => i.emit,
269269
$options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
270-
$forceUpdate: i => i.f || (i.f = () => queueJob(i.update)),
270+
$forceUpdate: i =>
271+
i.f ||
272+
(i.f = () => {
273+
i.effect.dirty = true
274+
queueJob(i.update)
275+
}),
271276
$nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),
272277
$watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
273278
} as PublicPropertiesMap)

‎packages/runtime-core/src/components/BaseTransition.ts

+1
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ const BaseTransitionImpl: ComponentOptions = {
246246
// #6835
247247
// it also needs to be updated when active is undefined
248248
if (instance.update.active !== false) {
249+
instance.effect.dirty = true
249250
instance.update()
250251
}
251252
}

‎packages/runtime-core/src/hmr.ts

+2
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ function rerender(id: string, newRender?: Function) {
9393
instance.renderCache = []
9494
// this flag forces child components with slot content to update
9595
isHmrUpdating = true
96+
instance.effect.dirty = true
9697
instance.update()
9798
isHmrUpdating = false
9899
})
@@ -137,6 +138,7 @@ function reload(id: string, newComp: HMRComponent) {
137138
// 4. Force the parent instance to re-render. This will cause all updated
138139
// components to be unmounted and re-mounted. Queue the update so that we
139140
// don't end up forcing the same parent to re-render multiple times.
141+
instance.parent.effect.dirty = true
140142
queueJob(instance.parent.update)
141143
} else if (instance.appContext.reload) {
142144
// root instance mounted via createApp() has a reload method

‎packages/runtime-core/src/renderer.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1280,6 +1280,7 @@ function baseCreateRenderer(
12801280
// double updating the same child component in the same flush.
12811281
invalidateJob(instance.update)
12821282
// instance.update is the reactive effect.
1283+
instance.effect.dirty = true
12831284
instance.update()
12841285
}
12851286
} else {
@@ -1544,11 +1545,16 @@ function baseCreateRenderer(
15441545
// create reactive effect for rendering
15451546
const effect = (instance.effect = new ReactiveEffect(
15461547
componentUpdateFn,
1548+
NOOP,
15471549
() => queueJob(update),
15481550
instance.scope // track it in component's effect scope
15491551
))
15501552

1551-
const update: SchedulerJob = (instance.update = () => effect.run())
1553+
const update: SchedulerJob = (instance.update = () => {
1554+
if (effect.dirty) {
1555+
effect.run()
1556+
}
1557+
})
15521558
update.id = instance.uid
15531559
// allowRecurse
15541560
// #1801, #2043 component render effects should allow recursive updates

0 commit comments

Comments
 (0)
Please sign in to comment.