Skip to content

Commit c795a2e

Browse files
skirtles-codeyyx990803
authored andcommitted
fix(reactivity): fix dep memory leak
1 parent 4162311 commit c795a2e

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
@@ -12,7 +12,7 @@ import {
1212
readonly,
1313
ReactiveEffectRunner
1414
} from '../src/index'
15-
import { ITERATE_KEY } from '../src/effect'
15+
import { getDepFromReactive, ITERATE_KEY } from '../src/effect'
1616

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

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
@@ -252,7 +263,7 @@ export function track(target: object, type: TrackOpTypes, key: unknown) {
252263
}
253264
let dep = depsMap.get(key)
254265
if (!dep) {
255-
depsMap.set(key, (dep = createDep()))
266+
depsMap.set(key, (dep = createDep(target, key)))
256267
}
257268

258269
const eventInfo = __DEV__
@@ -383,19 +394,19 @@ export function trigger(
383394
}
384395
}
385396
if (__DEV__) {
386-
triggerEffects(createDep(effects), eventInfo)
397+
triggerEffects(new Set(effects), eventInfo)
387398
} else {
388-
triggerEffects(createDep(effects))
399+
triggerEffects(new Set(effects))
389400
}
390401
}
391402
}
392403

393404
export function triggerEffects(
394-
dep: Dep | ReactiveEffect[],
405+
dep: Set<ReactiveEffect>,
395406
debuggerEventExtraInfo?: DebuggerEventExtraInfo
396407
) {
397408
// spread into array for stabilization
398-
const effects = isArray(dep) ? dep : [...dep]
409+
const effects = [...dep]
399410
for (const effect of effects) {
400411
if (effect.computed) {
401412
triggerEffect(effect, debuggerEventExtraInfo)

0 commit comments

Comments
 (0)