mirror of https://github.com/vuejs/core.git
fix(reactivity): fix property dep removal regression
close #12020 close #12021
This commit is contained in:
parent
c0e9434414
commit
6001e5c81a
|
@ -1023,6 +1023,7 @@ describe('reactivity/computed', () => {
|
|||
expect(p.value).toBe(3)
|
||||
})
|
||||
|
||||
// #11995
|
||||
test('computed dep cleanup should not cause property dep to be deleted', () => {
|
||||
const toggle = ref(true)
|
||||
const state = reactive({ a: 1 })
|
||||
|
@ -1037,4 +1038,23 @@ describe('reactivity/computed', () => {
|
|||
state.a++
|
||||
expect(pp.value).toBe(2)
|
||||
})
|
||||
|
||||
// #12020
|
||||
test('computed value updates correctly after dep cleanup', () => {
|
||||
const obj = reactive({ foo: 1, flag: 1 })
|
||||
const c1 = computed(() => obj.foo)
|
||||
|
||||
let foo
|
||||
effect(() => {
|
||||
foo = obj.flag ? (obj.foo, c1.value) : 0
|
||||
})
|
||||
expect(foo).toBe(1)
|
||||
|
||||
obj.flag = 0
|
||||
expect(foo).toBe(0)
|
||||
|
||||
obj.foo = 2
|
||||
obj.flag = 1
|
||||
expect(foo).toBe(2)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from '../src/reactive'
|
||||
import { computed } from '../src/computed'
|
||||
import { effect } from '../src/effect'
|
||||
import { targetMap } from '../src/dep'
|
||||
|
||||
describe('reactivity/reactive', () => {
|
||||
test('Object', () => {
|
||||
|
@ -398,4 +399,14 @@ describe('reactivity/reactive', () => {
|
|||
a.value++
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
// #11979
|
||||
test('should release property Dep instance if it no longer has subscribers', () => {
|
||||
let obj = { x: 1 }
|
||||
let a = reactive(obj)
|
||||
const e = effect(() => a.x)
|
||||
expect(targetMap.get(obj)?.get('x')).toBeTruthy()
|
||||
e.effect.stop()
|
||||
expect(targetMap.get(obj)?.get('x')).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -89,6 +89,11 @@ export class Dep {
|
|||
map?: KeyToDepMap = undefined
|
||||
key?: unknown = undefined
|
||||
|
||||
/**
|
||||
* Subscriber counter
|
||||
*/
|
||||
sc: number = 0
|
||||
|
||||
constructor(public computed?: ComputedRefImpl | undefined) {
|
||||
if (__DEV__) {
|
||||
this.subsHead = undefined
|
||||
|
@ -113,9 +118,7 @@ export class Dep {
|
|||
activeSub.depsTail = link
|
||||
}
|
||||
|
||||
if (activeSub.flags & EffectFlags.TRACKING) {
|
||||
addSub(link)
|
||||
}
|
||||
addSub(link)
|
||||
} else if (link.version === -1) {
|
||||
// reused from last run - already a sub, just sync version
|
||||
link.version = this.version
|
||||
|
@ -197,27 +200,30 @@ export class Dep {
|
|||
}
|
||||
|
||||
function addSub(link: Link) {
|
||||
const computed = link.dep.computed
|
||||
// computed getting its first subscriber
|
||||
// enable tracking + lazily subscribe to all its deps
|
||||
if (computed && !link.dep.subs) {
|
||||
computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
|
||||
for (let l = computed.deps; l; l = l.nextDep) {
|
||||
addSub(l)
|
||||
link.dep.sc++
|
||||
if (link.sub.flags & EffectFlags.TRACKING) {
|
||||
const computed = link.dep.computed
|
||||
// computed getting its first subscriber
|
||||
// enable tracking + lazily subscribe to all its deps
|
||||
if (computed && !link.dep.subs) {
|
||||
computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
|
||||
for (let l = computed.deps; l; l = l.nextDep) {
|
||||
addSub(l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentTail = link.dep.subs
|
||||
if (currentTail !== link) {
|
||||
link.prevSub = currentTail
|
||||
if (currentTail) currentTail.nextSub = link
|
||||
}
|
||||
const currentTail = link.dep.subs
|
||||
if (currentTail !== link) {
|
||||
link.prevSub = currentTail
|
||||
if (currentTail) currentTail.nextSub = link
|
||||
}
|
||||
|
||||
if (__DEV__ && link.dep.subsHead === undefined) {
|
||||
link.dep.subsHead = link
|
||||
}
|
||||
if (__DEV__ && link.dep.subsHead === undefined) {
|
||||
link.dep.subsHead = link
|
||||
}
|
||||
|
||||
link.dep.subs = link
|
||||
link.dep.subs = link
|
||||
}
|
||||
}
|
||||
|
||||
// The main WeakMap that stores {target -> key -> dep} connections.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { extend, hasChanged } from '@vue/shared'
|
||||
import type { ComputedRefImpl } from './computed'
|
||||
import type { TrackOpTypes, TriggerOpTypes } from './constants'
|
||||
import { type Link, globalVersion, targetMap } from './dep'
|
||||
import { type Link, globalVersion } from './dep'
|
||||
import { activeEffectScope } from './effectScope'
|
||||
import { warn } from './warning'
|
||||
|
||||
|
@ -292,7 +292,7 @@ function prepareDeps(sub: Subscriber) {
|
|||
}
|
||||
}
|
||||
|
||||
function cleanupDeps(sub: Subscriber, fromComputed = false) {
|
||||
function cleanupDeps(sub: Subscriber) {
|
||||
// Cleanup unsued deps
|
||||
let head
|
||||
let tail = sub.depsTail
|
||||
|
@ -302,7 +302,7 @@ function cleanupDeps(sub: Subscriber, fromComputed = false) {
|
|||
if (link.version === -1) {
|
||||
if (link === tail) tail = prev
|
||||
// unused - remove it from the dep's subscribing effect list
|
||||
removeSub(link, fromComputed)
|
||||
removeSub(link)
|
||||
// also remove it from this effect's dep list
|
||||
removeDep(link)
|
||||
} else {
|
||||
|
@ -394,12 +394,12 @@ export function refreshComputed(computed: ComputedRefImpl): undefined {
|
|||
} finally {
|
||||
activeSub = prevSub
|
||||
shouldTrack = prevShouldTrack
|
||||
cleanupDeps(computed, true)
|
||||
cleanupDeps(computed)
|
||||
computed.flags &= ~EffectFlags.RUNNING
|
||||
}
|
||||
}
|
||||
|
||||
function removeSub(link: Link, fromComputed = false) {
|
||||
function removeSub(link: Link, soft = false) {
|
||||
const { dep, prevSub, nextSub } = link
|
||||
if (prevSub) {
|
||||
prevSub.nextSub = nextSub
|
||||
|
@ -418,21 +418,24 @@ function removeSub(link: Link, fromComputed = false) {
|
|||
dep.subsHead = nextSub
|
||||
}
|
||||
|
||||
if (!dep.subs) {
|
||||
// last subscriber removed
|
||||
if (dep.computed) {
|
||||
// if computed, unsubscribe it from all its deps so this computed and its
|
||||
// value can be GCed
|
||||
dep.computed.flags &= ~EffectFlags.TRACKING
|
||||
for (let l = dep.computed.deps; l; l = l.nextDep) {
|
||||
removeSub(l, true)
|
||||
}
|
||||
} else if (dep.map && !fromComputed) {
|
||||
// property dep, remove it from the owner depsMap
|
||||
dep.map.delete(dep.key)
|
||||
if (!dep.map.size) targetMap.delete(dep.target!)
|
||||
if (!dep.subs && dep.computed) {
|
||||
// if computed, unsubscribe it from all its deps so this computed and its
|
||||
// value can be GCed
|
||||
dep.computed.flags &= ~EffectFlags.TRACKING
|
||||
for (let l = dep.computed.deps; l; l = l.nextDep) {
|
||||
// here we are only "soft" unsubscribing because the computed still keeps
|
||||
// referencing the deps and the dep should not decrease its sub count
|
||||
removeSub(l, true)
|
||||
}
|
||||
}
|
||||
|
||||
if (!soft && !--dep.sc && dep.map) {
|
||||
// #11979
|
||||
// property dep no longer has effect subscribers, delete it
|
||||
// this mostly is for the case where an object is kept in memory but only a
|
||||
// subset of its properties is tracked at one time
|
||||
dep.map.delete(dep.key)
|
||||
}
|
||||
}
|
||||
|
||||
function removeDep(link: Link) {
|
||||
|
|
Loading…
Reference in New Issue