Skip to content

Commit c3d54d6

Browse files
committed
fix(reactivity): fix dep memory leak
1 parent 650f5c2 commit c3d54d6

File tree

3 files changed

+97
-12
lines changed

3 files changed

+97
-12
lines changed

packages/reactivity/__tests__/effect.spec.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
readonly,
1414
ReactiveEffectRunner
1515
} from '../src/index'
16-
import { ITERATE_KEY } from '../src/effect'
16+
import { getDepFromReactive, ITERATE_KEY } from '../src/effect'
1717

1818
describe('reactivity/effect', () => {
1919
it('should run the passed function once (wrapped by a effect)', () => {
@@ -992,4 +992,68 @@ describe('reactivity/effect', () => {
992992
expect(has).toBe(false)
993993
})
994994
})
995+
996+
describe('empty dep cleanup', () => {
997+
it('should remove the dep when the effect is stopped', () => {
998+
const obj = reactive({ prop: 1 })
999+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
1000+
const runner = effect(() => obj.prop)
1001+
const dep = getDepFromReactive(toRaw(obj), 'prop')
1002+
expect(dep).toHaveLength(1)
1003+
obj.prop = 2
1004+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
1005+
expect(dep).toHaveLength(1)
1006+
stop(runner)
1007+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
1008+
obj.prop = 3
1009+
runner()
1010+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
1011+
})
1012+
1013+
it('should only remove the dep when the last effect is stopped', () => {
1014+
const obj = reactive({ prop: 1 })
1015+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
1016+
const runner1 = effect(() => obj.prop)
1017+
const dep = getDepFromReactive(toRaw(obj), 'prop')
1018+
expect(dep).toHaveLength(1)
1019+
const runner2 = effect(() => obj.prop)
1020+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
1021+
expect(dep).toHaveLength(2)
1022+
obj.prop = 2
1023+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
1024+
expect(dep).toHaveLength(2)
1025+
stop(runner1)
1026+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
1027+
expect(dep).toHaveLength(1)
1028+
obj.prop = 3
1029+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
1030+
expect(dep).toHaveLength(1)
1031+
stop(runner2)
1032+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
1033+
obj.prop = 4
1034+
runner1()
1035+
runner2()
1036+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
1037+
})
1038+
1039+
it('should remove the dep when it is no longer used by the effect', () => {
1040+
const obj = reactive<{ a: number; b: number; c: 'a' | 'b' }>({
1041+
a: 1,
1042+
b: 2,
1043+
c: 'a'
1044+
})
1045+
expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
1046+
effect(() => obj[obj.c])
1047+
const depC = getDepFromReactive(toRaw(obj), 'c')
1048+
expect(getDepFromReactive(toRaw(obj), 'a')).toHaveLength(1)
1049+
expect(getDepFromReactive(toRaw(obj), 'b')).toBeUndefined()
1050+
expect(depC).toHaveLength(1)
1051+
obj.c = 'b'
1052+
obj.a = 4
1053+
expect(getDepFromReactive(toRaw(obj), 'a')).toBeUndefined()
1054+
expect(getDepFromReactive(toRaw(obj), 'b')).toHaveLength(1)
1055+
expect(getDepFromReactive(toRaw(obj), 'c')).toBe(depC)
1056+
expect(depC).toHaveLength(1)
1057+
})
1058+
})
9951059
})

packages/reactivity/src/dep.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import { ReactiveEffect, trackOpBit } from './effect'
1+
import { ReactiveEffect, removeEffectFromDep, trackOpBit } from './effect'
22

3-
export type Dep = Set<ReactiveEffect> & TrackedMarkers
3+
export type Dep = Set<ReactiveEffect> &
4+
TrackedMarkers & {
5+
target?: unknown
6+
key?: unknown
7+
}
48

59
/**
610
* wasTracked and newTracked maintain the status for several levels of effect
@@ -18,10 +22,16 @@ type TrackedMarkers = {
1822
n: number
1923
}
2024

21-
export const createDep = (effects?: ReactiveEffect[]): Dep => {
22-
const dep = new Set<ReactiveEffect>(effects) as Dep
25+
export function createDep(): Dep
26+
export function createDep(target: unknown, key: unknown): Dep
27+
export function createDep(target?: unknown, key?: unknown): Dep {
28+
const dep = new Set<ReactiveEffect>() as Dep
2329
dep.w = 0
2430
dep.n = 0
31+
if (target) {
32+
dep.target = target
33+
dep.key = key
34+
}
2535
return dep
2636
}
2737

@@ -44,7 +54,7 @@ export const finalizeDepMarkers = (effect: ReactiveEffect) => {
4454
for (let i = 0; i < deps.length; i++) {
4555
const dep = deps[i]
4656
if (wasTracked(dep) && !newTracked(dep)) {
47-
dep.delete(effect)
57+
removeEffectFromDep(dep, effect)
4858
} else {
4959
deps[ptr++] = dep
5060
}

packages/reactivity/src/effect.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,23 @@ function cleanupEffect(effect: ReactiveEffect) {
143143
const { deps } = effect
144144
if (deps.length) {
145145
for (let i = 0; i < deps.length; i++) {
146-
deps[i].delete(effect)
146+
removeEffectFromDep(deps[i], effect)
147147
}
148148
deps.length = 0
149149
}
150150
}
151151

152+
export function removeEffectFromDep(dep: Dep, effect: ReactiveEffect) {
153+
dep.delete(effect)
154+
if (dep.target && dep.size === 0) {
155+
const depsMap = targetMap.get(dep.target)
156+
if (depsMap) {
157+
depsMap.delete(dep.key)
158+
}
159+
dep.target = dep.key = null
160+
}
161+
}
162+
152163
export interface DebuggerOptions {
153164
onTrack?: (event: DebuggerEvent) => void
154165
onTrigger?: (event: DebuggerEvent) => void
@@ -218,7 +229,7 @@ export function track(target: object, type: TrackOpTypes, key: unknown) {
218229
}
219230
let dep = depsMap.get(key)
220231
if (!dep) {
221-
depsMap.set(key, (dep = createDep()))
232+
depsMap.set(key, (dep = createDep(target, key)))
222233
}
223234

224235
const eventInfo = __DEV__
@@ -341,19 +352,19 @@ export function trigger(
341352
}
342353
}
343354
if (__DEV__) {
344-
triggerEffects(createDep(effects), eventInfo)
355+
triggerEffects(new Set(effects), eventInfo)
345356
} else {
346-
triggerEffects(createDep(effects))
357+
triggerEffects(new Set(effects))
347358
}
348359
}
349360
}
350361

351362
export function triggerEffects(
352-
dep: Dep | ReactiveEffect[],
363+
dep: Set<ReactiveEffect>,
353364
debuggerEventExtraInfo?: DebuggerEventExtraInfo
354365
) {
355366
// spread into array for stabilization
356-
const effects = isArray(dep) ? dep : [...dep]
367+
const effects = [...dep]
357368
for (const effect of effects) {
358369
if (effect.computed) {
359370
triggerEffect(effect, debuggerEventExtraInfo)

0 commit comments

Comments
 (0)