Просмотр исходного кода

fix(ssr): avoid triggering onScopeDispose during SSR cleanup

daiwei 3 недель назад
Родитель
Сommit
4766fe4a7a

+ 6 - 4
packages/reactivity/src/effectScope.ts

@@ -129,7 +129,7 @@ export class EffectScope {
     }
   }
 
-  stop(fromParent?: boolean): void {
+  stop(fromParent?: boolean, allowScopeCleanups = true): void {
     if (this._active) {
       this._active = false
       let i, l
@@ -138,14 +138,16 @@ export class EffectScope {
       }
       this.effects.length = 0
 
-      for (i = 0, l = this.cleanups.length; i < l; i++) {
-        this.cleanups[i]()
+      if (allowScopeCleanups) {
+        for (i = 0, l = this.cleanups.length; i < l; i++) {
+          this.cleanups[i]()
+        }
       }
       this.cleanups.length = 0
 
       if (this.scopes) {
         for (i = 0, l = this.scopes.length; i < l; i++) {
-          this.scopes[i].stop(true)
+          this.scopes[i].stop(true, allowScopeCleanups)
         }
         this.scopes.length = 0
       }

+ 13 - 6
packages/server-renderer/__tests__/render.spec.ts

@@ -1004,10 +1004,12 @@ function testRender(type: string, render: typeof renderToString) {
       expect(html).toBe(`<div>hello</div>`)
     })
 
-    test('cleans up component effect scopes after each render', async () => {
+    test('stops component effect scopes after each render without triggering onScopeDispose', async () => {
+      let instanceScope: any
       const cleanups: number[] = []
       const app = createApp({
         setup() {
+          instanceScope = getCurrentInstance()!.scope
           onScopeDispose(() => {
             cleanups.push(1)
           })
@@ -1015,13 +1017,14 @@ function testRender(type: string, render: typeof renderToString) {
         },
       })
 
-      expect(cleanups).toEqual([])
       expect(await render(app)).toBe(`<div>ok</div>`)
-      expect(cleanups).toEqual([1])
+      expect(instanceScope.active).toBe(false)
+      expect(cleanups).toEqual([])
     })
 
-    test('concurrent renders isolate scope cleanup ownership', async () => {
+    test('concurrent renders isolate effect scope disposal ownership', async () => {
       const cleaned: string[] = []
+      const scopes: Record<string, any> = {}
 
       const deferred = () => {
         let resolve!: () => void
@@ -1037,6 +1040,7 @@ function testRender(type: string, render: typeof renderToString) {
       const makeApp = (id: string, gate: ReturnType<typeof deferred>) =>
         createApp({
           async setup() {
+            scopes[id] = getCurrentInstance()!.scope
             onScopeDispose(() => {
               cleaned.push(id)
             })
@@ -1050,11 +1054,14 @@ function testRender(type: string, render: typeof renderToString) {
 
       gateB.resolve()
       expect(await pB).toBe(`<div>B</div>`)
-      expect(cleaned).toEqual(['B'])
+      expect(scopes.B.active).toBe(false)
+      expect(scopes.A.active).toBe(true)
+      expect(cleaned).toEqual([])
 
       gateA.resolve()
       expect(await pA).toBe(`<div>A</div>`)
-      expect(cleaned.sort()).toEqual(['A', 'B'])
+      expect(scopes.A.active).toBe(false)
+      expect(cleaned).toEqual([])
     })
 
     test('detached scopes created during SSR are not auto-stopped', async () => {

+ 6 - 2
packages/server-renderer/src/render.ts

@@ -59,7 +59,9 @@ export type SSRContext = {
   /**
    * @internal
    */
-  __instanceScopes?: { stop: () => void }[]
+  __instanceScopes?: {
+    stop: (fromParent?: boolean, allowScopeCleanups?: boolean) => void
+  }[]
 }
 
 export function cleanupContext(context: SSRContext): void {
@@ -77,7 +79,9 @@ export function cleanupContext(context: SSRContext): void {
   if (context.__instanceScopes) {
     for (const scope of context.__instanceScopes) {
       try {
-        scope.stop()
+        // SSR should dispose component-owned effects/scopes without invoking
+        // user cleanup callbacks registered via onScopeDispose().
+        scope.stop(false, false)
       } catch (err) {
         if (firstError === undefined) firstError = err
       }