Quellcode durchsuchen

fix(reactivity): cleanup unsubscribed computed deps and release stale ref oldValue

daiwei vor 2 Monaten
Ursprung
Commit
84b6832ed2

+ 2 - 2
packages/reactivity/__tests__/gc.spec.ts

@@ -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!)
@@ -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)

+ 46 - 0
packages/reactivity/src/computed.ts

@@ -14,6 +14,7 @@ import {
   link,
   shallowPropagate,
   startTracking,
+  unlink,
 } from './system'
 import { warn } from './warning'
 
@@ -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
@@ -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!
   }
@@ -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

+ 5 - 0
packages/reactivity/src/ref.ts

@@ -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
+  }
 }
 
 /**

+ 5 - 0
packages/reactivity/src/system.ts

@@ -10,6 +10,7 @@ export interface ReactiveNode {
   subs?: Link
   subsTail?: Link
   flags: ReactiveFlags
+  cleanup?: () => void
 }
 
 export interface Link {
@@ -35,6 +36,7 @@ export const enum ReactiveFlags {
   Recursed = 1 << 3,
   Dirty = 1 << 4,
   Pending = 1 << 5,
+  CleanupScheduled = 1 << 6,
 }
 
 const notifyBuffer: (Effect | undefined)[] = []
@@ -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 {