Kaynağa Gözat

fix(runtime-core): prevent currentInstance leak into sibling render during async setup re-entry (#14668)

fix #14667
Matej Černý 2 hafta önce
ebeveyn
işleme
f1663535a1

+ 64 - 0
packages/runtime-core/__tests__/apiSetupHelpers.spec.ts

@@ -330,6 +330,70 @@ describe('SFC <script setup> helpers', () => {
       expect(seenUid.two).toBeNull()
     })
 
+    test('should not leak currentInstance to sibling slot render', async () => {
+      let done!: () => void
+      const ready = new Promise<void>(r => {
+        done = r
+      })
+      let innerUid: number | null = null
+      let innerRenderUid: number | null = null
+
+      const Inner = defineComponent({
+        setup(_, { slots }) {
+          innerUid = getCurrentInstance()!.uid
+          return () => {
+            innerRenderUid = getCurrentInstance()!.uid
+            done()
+            return h('div', slots.default?.())
+          }
+        },
+      })
+
+      const Outer = defineComponent({
+        setup(_, { slots }) {
+          return () => h(Inner, null, () => [slots.default?.()])
+        },
+      })
+
+      const AsyncA = defineComponent({
+        async setup() {
+          let __temp: any, __restore: any
+          ;[__temp, __restore] = withAsyncContext(() =>
+            Promise.resolve()
+              .then(() => {})
+              .then(() => {}),
+          )
+          __temp = await __temp
+          __restore()
+          return () => h('div', 'A')
+        },
+      })
+
+      const AsyncB = defineComponent({
+        async setup() {
+          let __temp: any, __restore: any
+          ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
+          __temp = await __temp
+          __restore()
+          return () => h(Outer, null, () => 'B')
+        },
+      })
+
+      const root = nodeOps.createElement('div')
+      render(
+        h(() => h(Suspense, () => h('div', [h(AsyncA), h(AsyncB)]))),
+        root,
+      )
+
+      await ready
+      expect(
+        'Slot "default" invoked outside of the render function',
+      ).not.toHaveBeenWarned()
+      expect(innerRenderUid).toBe(innerUid)
+      await Promise.resolve()
+      expect(serializeInner(root)).toBe(`<div><div>A</div><div>B</div></div>`)
+    })
+
     test('error handling', async () => {
       const spy = vi.fn()
 

+ 9 - 1
packages/runtime-core/src/components/Suspense.ts

@@ -11,7 +11,11 @@ import {
   openBlock,
 } from '../vnode'
 import { ShapeFlags, isArray, isFunction, toNumber } from '@vue/shared'
-import { type ComponentInternalInstance, handleSetupResult } from '../component'
+import {
+  type ComponentInternalInstance,
+  handleSetupResult,
+  unsetCurrentInstance,
+} from '../component'
 import type { Slots } from '../componentSlots'
 import {
   type ElementNamespace,
@@ -722,6 +726,10 @@ function createSuspenseBoundary(
           ) {
             return
           }
+          // withAsyncContext defers cleanup to a later microtask, so currentInstance may
+          // still be set when Suspense re-enters another component's render path.
+          // Clear it first.
+          unsetCurrentInstance()
           // retry from this component
           instance.asyncResolved = true
           const { vnode } = instance