Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
fix(reactivity): cleanup unsubscribed computed deps and release stale…
… ref oldValue
  • Loading branch information
edison1105 committed Feb 10, 2026
commit 84b6832ed26ebec4f0462b06a33fb71e13e78566
4 changes: 2 additions & 2 deletions packages/reactivity/__tests__/gc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
}

// #9233
it.todo('should release computed cache', async () => {
it('should release computed cache', async () => {
const src = ref<{} | undefined>({})
// @ts-expect-error ES2021 API
const srcRef = new WeakRef(src.value!)
Expand All @@ -35,7 +35,7 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
expect(srcRef.deref()).toBeUndefined()
})

it.todo('should release reactive property dep', async () => {
it('should release reactive property dep', async () => {
const src = reactive({ foo: 1 })

let c: ComputedRef | undefined = computed(() => src.foo)
Expand Down
46 changes: 46 additions & 0 deletions packages/reactivity/src/computed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
link,
shallowPropagate,
startTracking,
unlink,
} from './system'
import { warn } from './warning'

Expand Down Expand Up @@ -60,6 +61,7 @@ export class ComputedRefImpl<T = any> implements ReactiveNode {
depsTail: Link | undefined = undefined
flags: SystemReactiveFlags =
SystemReactiveFlags.Mutable | SystemReactiveFlags.Dirty
cleanupNext: ComputedRefImpl | undefined = undefined

/**
* @internal
Expand Down Expand Up @@ -149,6 +151,11 @@ export class ComputedRefImpl<T = any> implements ReactiveNode {
link(this, activeSub)
} else if (activeEffectScope !== undefined) {
link(this, activeEffectScope)
} else if (
this.subs === undefined &&
!(this.flags & SystemReactiveFlags.CleanupScheduled)
) {
scheduleCleanup(this)
}
return this._value!
}
Expand Down Expand Up @@ -181,6 +188,45 @@ if (__DEV__) {
setupOnTrigger(ComputedRefImpl)
}

let cleanupHead: ComputedRefImpl | undefined = undefined
let cleanupTail: ComputedRefImpl | undefined = undefined
let isFlushing = false
const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise<void>

function scheduleCleanup(c: ComputedRefImpl): void {
c.flags |= SystemReactiveFlags.CleanupScheduled
if (cleanupTail !== undefined) {
cleanupTail.cleanupNext = c
cleanupTail = c
} else {
cleanupHead = cleanupTail = c
}
if (!isFlushing) {
isFlushing = true
resolvedPromise.then(cleanup)
}
}

// clean up unsubscribed computed refs after a tick
function cleanup(): void {
let c = cleanupHead
cleanupHead = cleanupTail = undefined
isFlushing = false
while (c !== undefined) {
const next = c.cleanupNext
c.cleanupNext = undefined
c.flags &= ~SystemReactiveFlags.CleanupScheduled
if (c.subs === undefined) {
let dep = c.deps
while (dep !== undefined) {
dep = unlink(dep, c)
}
c.flags |= SystemReactiveFlags.Dirty
}
c = next
}
}

/**
* Takes a getter function and returns a readonly reactive ref object for the
* returned value from the getter. It can also take an object with get and set
Expand Down
5 changes: 5 additions & 0 deletions packages/reactivity/src/ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,11 @@ class RefImpl<T = any> implements ReactiveNode {
this.flags &= ~_ReactiveFlags.Dirty
return hasChanged(this._oldValue, (this._oldValue = this._rawValue))
}

// Release stale old-value references when nothing depends on this ref.
cleanup(): void {
this._oldValue = this._rawValue
}
}

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/reactivity/src/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface ReactiveNode {
subs?: Link
subsTail?: Link
flags: ReactiveFlags
cleanup?: () => void
}

export interface Link {
Expand All @@ -35,6 +36,7 @@ export const enum ReactiveFlags {
Recursed = 1 << 3,
Dirty = 1 << 4,
Pending = 1 << 5,
CleanupScheduled = 1 << 6,
}

const notifyBuffer: (Effect | undefined)[] = []
Expand Down Expand Up @@ -137,6 +139,9 @@ export function unlink(
if (prevSub !== undefined) {
prevSub.nextSub = nextSub
} else if ((dep.subs = nextSub) === undefined) {
if ((dep as ReactiveNode).cleanup !== undefined) {
;(dep as ReactiveNode).cleanup!()
}
let toRemove = dep.deps
if (toRemove !== undefined) {
do {
Expand Down
Loading