]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(reactivity): avoid exponential perf cost and reduce call stack depth for deeply...
authorEvan You <evan@vuejs.org>
Mon, 16 Sep 2024 08:00:31 +0000 (16:00 +0800)
committerGitHub <noreply@github.com>
Mon, 16 Sep 2024 08:00:31 +0000 (16:00 +0800)
close #11928

packages/reactivity/src/computed.ts
packages/reactivity/src/dep.ts
packages/reactivity/src/effect.ts

index a5f8e5a3c2b7d8ea934a92a12b6ad1e5407c570e..b16b011c5b75ba7e99ecd84473548b0b505f8455 100644 (file)
@@ -5,6 +5,7 @@ import {
   EffectFlags,
   type Subscriber,
   activeSub,
+  batch,
   refreshComputed,
 } from './effect'
 import type { Ref } from './ref'
@@ -109,11 +110,15 @@ export class ComputedRefImpl<T = any> implements Subscriber {
   /**
    * @internal
    */
-  notify(): void {
+  notify(): true | void {
     this.flags |= EffectFlags.DIRTY
-    // avoid infinite self recursion
-    if (activeSub !== this) {
-      this.dep.notify()
+    if (
+      !(this.flags & EffectFlags.NOTIFIED) &&
+      // avoid infinite self recursion
+      activeSub !== this
+    ) {
+      batch(this)
+      return true
     } else if (__DEV__) {
       // TODO warn
     }
index 8e4ad1e649ed7273c71e4b27779bc9ed74b8de4f..c24f123ded49b670983d3cc6c957965d65fd7c9e 100644 (file)
@@ -163,11 +163,7 @@ export class Dep {
         // original order at the end of the batch, but onTrigger hooks should
         // be invoked in original order here.
         for (let head = this.subsHead; head; head = head.nextSub) {
-          if (
-            __DEV__ &&
-            head.sub.onTrigger &&
-            !(head.sub.flags & EffectFlags.NOTIFIED)
-          ) {
+          if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) {
             head.sub.onTrigger(
               extend(
                 {
@@ -180,7 +176,12 @@ export class Dep {
         }
       }
       for (let link = this.subs; link; link = link.prevSub) {
-        link.sub.notify()
+        if (link.sub.notify()) {
+          // if notify() returns `true`, this is a computed. Also call notify
+          // on its dep - it's called here instead of inside computed's notify
+          // in order to reduce call stack depth.
+          ;(link.sub as ComputedRefImpl).dep.notify()
+        }
       }
     } finally {
       endBatch()
index 2dae285d1665be96bcdd14702fcefdd618da0e59..b8dd62a0f6ee7253b6393cb19f92170a3d8b1e0b 100644 (file)
@@ -39,6 +39,9 @@ export interface ReactiveEffectRunner<T = any> {
 export let activeSub: Subscriber | undefined
 
 export enum EffectFlags {
+  /**
+   * ReactiveEffect only
+   */
   ACTIVE = 1 << 0,
   RUNNING = 1 << 1,
   TRACKING = 1 << 2,
@@ -69,7 +72,13 @@ export interface Subscriber extends DebuggerOptions {
   /**
    * @internal
    */
-  notify(): void
+  next?: Subscriber
+  /**
+   * returning `true` indicates it's a computed that needs to call notify
+   * on its dep too
+   * @internal
+   */
+  notify(): true | void
 }
 
 const pausedQueueEffects = new WeakSet<ReactiveEffect>()
@@ -92,7 +101,7 @@ export class ReactiveEffect<T = any>
   /**
    * @internal
    */
-  nextEffect?: ReactiveEffect = undefined
+  next?: Subscriber = undefined
   /**
    * @internal
    */
@@ -134,9 +143,7 @@ export class ReactiveEffect<T = any>
       return
     }
     if (!(this.flags & EffectFlags.NOTIFIED)) {
-      this.flags |= EffectFlags.NOTIFIED
-      this.nextEffect = batchedEffect
-      batchedEffect = this
+      batch(this)
     }
   }
 
@@ -226,7 +233,13 @@ export class ReactiveEffect<T = any>
 // }
 
 let batchDepth = 0
-let batchedEffect: ReactiveEffect | undefined
+let batchedSub: Subscriber | undefined
+
+export function batch(sub: Subscriber): void {
+  sub.flags |= EffectFlags.NOTIFIED
+  sub.next = batchedSub
+  batchedSub = sub
+}
 
 /**
  * @internal
@@ -245,16 +258,17 @@ export function endBatch(): void {
   }
 
   let error: unknown
-  while (batchedEffect) {
-    let e: ReactiveEffect | undefined = batchedEffect
-    batchedEffect = undefined
+  while (batchedSub) {
+    let e: Subscriber | undefined = batchedSub
+    batchedSub = undefined
     while (e) {
-      const next: ReactiveEffect | undefined = e.nextEffect
-      e.nextEffect = undefined
+      const next: Subscriber | undefined = e.next
+      e.next = undefined
       e.flags &= ~EffectFlags.NOTIFIED
       if (e.flags & EffectFlags.ACTIVE) {
         try {
-          e.trigger()
+          // ACTIVE flag is effect-only
+          ;(e as ReactiveEffect).trigger()
         } catch (err) {
           if (!error) error = err
         }