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

fix(teleport): should not mount deferred teleport after unmount (#14598)

edison 3 месяцев назад
Родитель
Сommit
e473e23b4c

+ 29 - 0
packages/runtime-vapor/__tests__/components/Teleport.spec.ts

@@ -130,6 +130,35 @@ describe('renderer: VaporTeleport', () => {
         `<!--teleport start--><!--teleport end--><div>Footer</div><div id="targetId"><div>bar</div></div><!--if-->`,
       )
     })
+
+    test('should not mount deferred teleport content after unmount', async () => {
+      const root = document.createElement('div')
+      const target = document.createElement('div')
+      const show = ref(true)
+      const { mount } = define({
+        setup() {
+          return createIf(
+            () => show.value,
+            () =>
+              createComp(
+                VaporTeleport,
+                {
+                  to: () => target,
+                  defer: () => true,
+                },
+                { default: () => template('<div>teleported</div>')() },
+              ),
+            () => template('<div>root</div>')(),
+          )
+        },
+      }).create()
+      mount(root)
+
+      show.value = false
+      await nextTick()
+
+      expect(target.innerHTML).toBe('')
+    })
   })
 
   describe('HMR', () => {

+ 24 - 1
packages/runtime-vapor/src/components/Teleport.ts

@@ -3,6 +3,8 @@ import {
   type GenericComponentInstance,
   MismatchTypes,
   MoveType,
+  type SchedulerJob,
+  SchedulerJobFlags,
   type TeleportProps,
   type TeleportTargetElement,
   isMismatchAllowed,
@@ -82,6 +84,8 @@ export class TeleportFragment extends VaporFragment {
   mountContainer?: ParentNode | null
   mountAnchor?: Node | null
 
+  private mountToTargetJob?: SchedulerJob
+
   constructor(props: LooseRawProps, slots?: LooseRawSlots | null) {
     super([])
     this.rawProps = props
@@ -273,7 +277,21 @@ export class TeleportFragment extends VaporFragment {
         // typically due to an early insertion caused by setInsertionState.
         !this.parent!.isConnected
       ) {
-        queuePostFlushCb(this.mountToTarget.bind(this))
+        // Reuse one queued mount job per Teleport instance so repeated
+        // updates in the same flush don't enqueue duplicate target mounts.
+        // If the previous job was disposed during unmount, recreate it.
+        if (
+          !this.mountToTargetJob ||
+          this.mountToTargetJob.flags! & SchedulerJobFlags.DISPOSED
+        ) {
+          this.mountToTargetJob = () => {
+            this.mountToTargetJob = undefined
+            // State may have changed before the post-flush job runs.
+            if (this.isDisabled || !this.anchor) return
+            this.mountToTarget()
+          }
+        }
+        queuePostFlushCb(this.mountToTargetJob)
       } else {
         this.mountToTarget()
       }
@@ -297,6 +315,11 @@ export class TeleportFragment extends VaporFragment {
   }
 
   remove = (parent: ParentNode | undefined = this.parent!): void => {
+    if (this.mountToTargetJob) {
+      this.mountToTargetJob.flags! |= SchedulerJobFlags.DISPOSED
+      this.mountToTargetJob = undefined
+    }
+
     // remove nodes
     if (this.nodes && this.mountContainer) {
       remove(this.nodes, this.mountContainer)