Skip to content

Commit 6001e5c

Browse files
committed
fix(reactivity): fix property dep removal regression
close #12020 close #12021
1 parent c0e9434 commit 6001e5c

File tree

4 files changed

+78
-38
lines changed

4 files changed

+78
-38
lines changed

packages/reactivity/__tests__/computed.spec.ts

+20
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,7 @@ describe('reactivity/computed', () => {
10231023
expect(p.value).toBe(3)
10241024
})
10251025

1026+
// #11995
10261027
test('computed dep cleanup should not cause property dep to be deleted', () => {
10271028
const toggle = ref(true)
10281029
const state = reactive({ a: 1 })
@@ -1037,4 +1038,23 @@ describe('reactivity/computed', () => {
10371038
state.a++
10381039
expect(pp.value).toBe(2)
10391040
})
1041+
1042+
// #12020
1043+
test('computed value updates correctly after dep cleanup', () => {
1044+
const obj = reactive({ foo: 1, flag: 1 })
1045+
const c1 = computed(() => obj.foo)
1046+
1047+
let foo
1048+
effect(() => {
1049+
foo = obj.flag ? (obj.foo, c1.value) : 0
1050+
})
1051+
expect(foo).toBe(1)
1052+
1053+
obj.flag = 0
1054+
expect(foo).toBe(0)
1055+
1056+
obj.foo = 2
1057+
obj.flag = 1
1058+
expect(foo).toBe(2)
1059+
})
10401060
})

packages/reactivity/__tests__/reactive.spec.ts

+11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from '../src/reactive'
1414
import { computed } from '../src/computed'
1515
import { effect } from '../src/effect'
16+
import { targetMap } from '../src/dep'
1617

1718
describe('reactivity/reactive', () => {
1819
test('Object', () => {
@@ -398,4 +399,14 @@ describe('reactivity/reactive', () => {
398399
a.value++
399400
}).not.toThrow()
400401
})
402+
403+
// #11979
404+
test('should release property Dep instance if it no longer has subscribers', () => {
405+
let obj = { x: 1 }
406+
let a = reactive(obj)
407+
const e = effect(() => a.x)
408+
expect(targetMap.get(obj)?.get('x')).toBeTruthy()
409+
e.effect.stop()
410+
expect(targetMap.get(obj)?.get('x')).toBeFalsy()
411+
})
401412
})

packages/reactivity/src/dep.ts

+26-20
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ export class Dep {
8989
map?: KeyToDepMap = undefined
9090
key?: unknown = undefined
9191

92+
/**
93+
* Subscriber counter
94+
*/
95+
sc: number = 0
96+
9297
constructor(public computed?: ComputedRefImpl | undefined) {
9398
if (__DEV__) {
9499
this.subsHead = undefined
@@ -113,9 +118,7 @@ export class Dep {
113118
activeSub.depsTail = link
114119
}
115120

116-
if (activeSub.flags & EffectFlags.TRACKING) {
117-
addSub(link)
118-
}
121+
addSub(link)
119122
} else if (link.version === -1) {
120123
// reused from last run - already a sub, just sync version
121124
link.version = this.version
@@ -197,27 +200,30 @@ export class Dep {
197200
}
198201

199202
function addSub(link: Link) {
200-
const computed = link.dep.computed
201-
// computed getting its first subscriber
202-
// enable tracking + lazily subscribe to all its deps
203-
if (computed && !link.dep.subs) {
204-
computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
205-
for (let l = computed.deps; l; l = l.nextDep) {
206-
addSub(l)
203+
link.dep.sc++
204+
if (link.sub.flags & EffectFlags.TRACKING) {
205+
const computed = link.dep.computed
206+
// computed getting its first subscriber
207+
// enable tracking + lazily subscribe to all its deps
208+
if (computed && !link.dep.subs) {
209+
computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
210+
for (let l = computed.deps; l; l = l.nextDep) {
211+
addSub(l)
212+
}
207213
}
208-
}
209214

210-
const currentTail = link.dep.subs
211-
if (currentTail !== link) {
212-
link.prevSub = currentTail
213-
if (currentTail) currentTail.nextSub = link
214-
}
215+
const currentTail = link.dep.subs
216+
if (currentTail !== link) {
217+
link.prevSub = currentTail
218+
if (currentTail) currentTail.nextSub = link
219+
}
215220

216-
if (__DEV__ && link.dep.subsHead === undefined) {
217-
link.dep.subsHead = link
218-
}
221+
if (__DEV__ && link.dep.subsHead === undefined) {
222+
link.dep.subsHead = link
223+
}
219224

220-
link.dep.subs = link
225+
link.dep.subs = link
226+
}
221227
}
222228

223229
// The main WeakMap that stores {target -> key -> dep} connections.

packages/reactivity/src/effect.ts

+21-18
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { extend, hasChanged } from '@vue/shared'
22
import type { ComputedRefImpl } from './computed'
33
import type { TrackOpTypes, TriggerOpTypes } from './constants'
4-
import { type Link, globalVersion, targetMap } from './dep'
4+
import { type Link, globalVersion } from './dep'
55
import { activeEffectScope } from './effectScope'
66
import { warn } from './warning'
77

@@ -292,7 +292,7 @@ function prepareDeps(sub: Subscriber) {
292292
}
293293
}
294294

295-
function cleanupDeps(sub: Subscriber, fromComputed = false) {
295+
function cleanupDeps(sub: Subscriber) {
296296
// Cleanup unsued deps
297297
let head
298298
let tail = sub.depsTail
@@ -302,7 +302,7 @@ function cleanupDeps(sub: Subscriber, fromComputed = false) {
302302
if (link.version === -1) {
303303
if (link === tail) tail = prev
304304
// unused - remove it from the dep's subscribing effect list
305-
removeSub(link, fromComputed)
305+
removeSub(link)
306306
// also remove it from this effect's dep list
307307
removeDep(link)
308308
} else {
@@ -394,12 +394,12 @@ export function refreshComputed(computed: ComputedRefImpl): undefined {
394394
} finally {
395395
activeSub = prevSub
396396
shouldTrack = prevShouldTrack
397-
cleanupDeps(computed, true)
397+
cleanupDeps(computed)
398398
computed.flags &= ~EffectFlags.RUNNING
399399
}
400400
}
401401

402-
function removeSub(link: Link, fromComputed = false) {
402+
function removeSub(link: Link, soft = false) {
403403
const { dep, prevSub, nextSub } = link
404404
if (prevSub) {
405405
prevSub.nextSub = nextSub
@@ -418,21 +418,24 @@ function removeSub(link: Link, fromComputed = false) {
418418
dep.subsHead = nextSub
419419
}
420420

421-
if (!dep.subs) {
422-
// last subscriber removed
423-
if (dep.computed) {
424-
// if computed, unsubscribe it from all its deps so this computed and its
425-
// value can be GCed
426-
dep.computed.flags &= ~EffectFlags.TRACKING
427-
for (let l = dep.computed.deps; l; l = l.nextDep) {
428-
removeSub(l, true)
429-
}
430-
} else if (dep.map && !fromComputed) {
431-
// property dep, remove it from the owner depsMap
432-
dep.map.delete(dep.key)
433-
if (!dep.map.size) targetMap.delete(dep.target!)
421+
if (!dep.subs && dep.computed) {
422+
// if computed, unsubscribe it from all its deps so this computed and its
423+
// value can be GCed
424+
dep.computed.flags &= ~EffectFlags.TRACKING
425+
for (let l = dep.computed.deps; l; l = l.nextDep) {
426+
// here we are only "soft" unsubscribing because the computed still keeps
427+
// referencing the deps and the dep should not decrease its sub count
428+
removeSub(l, true)
434429
}
435430
}
431+
432+
if (!soft && !--dep.sc && dep.map) {
433+
// #11979
434+
// property dep no longer has effect subscribers, delete it
435+
// this mostly is for the case where an object is kept in memory but only a
436+
// subset of its properties is tracked at one time
437+
dep.map.delete(dep.key)
438+
}
436439
}
437440

438441
function removeDep(link: Link) {

0 commit comments

Comments
 (0)