Procházet zdrojové kódy

fix(teleport): remove stale target anchors after invalid target updates (#14600)

edison před 3 měsíci
rodič
revize
e7828d438c

+ 31 - 0
packages/runtime-core/__tests__/components/Teleport.spec.ts

@@ -292,6 +292,37 @@ describe('renderer: teleport', () => {
       expect(serializeInner(targetB)).toBe(`<div>teleported</div>`)
     })
 
+    test('should clean up anchors when target becomes invalid', async () => {
+      const targetA = nodeOps.createElement('div')
+      const to = ref<any>(targetA)
+      const root = nodeOps.createElement('div')
+
+      render(
+        h(() => h(Teleport, { to: to.value }, h('div', 'teleported'))),
+        root,
+      )
+
+      expect(serializeInner(root)).toBe(
+        `<!--teleport start--><!--teleport end-->`,
+      )
+      expect(serializeInner(targetA)).toBe(`<div>teleported</div>`)
+      expect(targetA.children.length).toBe(3)
+
+      to.value = null
+      await nextTick()
+      expect('Invalid Teleport target').toHaveBeenWarned()
+
+      expect(serializeInner(root)).toBe(
+        `<!--teleport start--><!--teleport end-->`,
+      )
+      expect(serializeInner(targetA)).toBe(`<div>teleported</div>`)
+      expect(targetA.children.length).toBe(3)
+
+      render(null, root)
+      expect(serializeInner(targetA)).toBe(``)
+      expect(targetA.children.length).toBe(0)
+    })
+
     test('move cached text nodes', async () => {
       document.body.innerHTML = ''
       const root = document.createElement('div')

+ 8 - 13
packages/runtime-core/src/components/Teleport.ts

@@ -317,19 +317,14 @@ export const TeleportImpl = {
     { um: unmount, o: { remove: hostRemove } }: RendererInternals,
     doRemove: boolean,
   ): void {
-    const {
-      shapeFlag,
-      children,
-      anchor,
-      targetStart,
-      targetAnchor,
-      target,
-      props,
-    } = vnode
-
-    if (target) {
-      hostRemove(targetStart!)
-      hostRemove(targetAnchor!)
+    const { shapeFlag, children, anchor, targetStart, targetAnchor, props } =
+      vnode
+
+    if (targetStart) {
+      hostRemove(targetStart)
+    }
+    if (targetAnchor) {
+      hostRemove(targetAnchor)
     }
 
     // an unmounted teleport should always unmount its children whether it's disabled or not

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

@@ -837,6 +837,51 @@ function runSharedTests(deferMode: boolean): void {
     expect(targetB.innerHTML).toBe('<div>teleported</div>')
   })
 
+  test('should clean up anchors when target becomes invalid', async () => {
+    const targetA = document.createElement('div')
+    const target = ref<any>(targetA)
+    const root = document.createElement('div')
+
+    const { app } = define({
+      setup() {
+        const n0 = createComponent(
+          VaporTeleport,
+          {
+            to: () => target.value,
+          },
+          {
+            default: () => template('<div>teleported</div>')(),
+          },
+        )
+        const n1 = template('<div>root</div>')()
+        return [n0, n1]
+      },
+    }).create()
+
+    app.mount(root)
+
+    expect(root.innerHTML).toBe(
+      '<!--teleport start--><!--teleport end--><div>root</div>',
+    )
+    expect(targetA.innerHTML).toBe('<div>teleported</div>')
+    expect(targetA.childNodes.length).toBe(3)
+
+    target.value = '#missing-teleport-target'
+    await nextTick()
+    expect('Failed to locate Teleport target').toHaveBeenWarned()
+    expect('Invalid Teleport target on update').toHaveBeenWarned()
+
+    expect(root.innerHTML).toBe(
+      '<!--teleport start--><!--teleport end--><div>root</div>',
+    )
+    expect(targetA.innerHTML).toBe('<div>teleported</div>')
+    expect(targetA.childNodes.length).toBe(3)
+
+    app.unmount()
+    expect(targetA.innerHTML).toBe('')
+    expect(targetA.childNodes.length).toBe(0)
+  })
+
   test('should update children', async () => {
     const target = document.createElement('div')
     const root = document.createElement('div')

+ 14 - 9
packages/runtime-vapor/src/components/Teleport.ts

@@ -23,7 +23,12 @@ import {
   move,
   remove,
 } from '../block'
-import { createComment, createTextNode, querySelector } from '../dom/node'
+import {
+  createComment,
+  createTextNode,
+  parentNode,
+  querySelector,
+} from '../dom/node'
 import {
   type LooseRawProps,
   type LooseRawSlots,
@@ -120,7 +125,7 @@ export class TeleportFragment extends VaporFragment {
   }
 
   get parent(): ParentNode | null {
-    return this.anchor ? this.anchor.parentNode : null
+    return this.anchor ? parentNode(this.anchor) : null
   }
 
   private initChildren(): void {
@@ -227,14 +232,14 @@ export class TeleportFragment extends VaporFragment {
         // initial mount into target
         !this.targetAnchor ||
         // target changed
-        this.targetAnchor.parentNode !== target
+        parentNode(this.targetAnchor) !== target
       ) {
         // clean up old anchors from previous target when target changes
         if (this.targetStart) {
-          remove(this.targetStart, this.targetStart.parentNode!)
+          remove(this.targetStart, parentNode(this.targetStart)!)
         }
         if (this.targetAnchor) {
-          remove(this.targetAnchor, this.targetAnchor.parentNode!)
+          remove(this.targetAnchor, parentNode(this.targetAnchor)!)
         }
         insert((this.targetStart = createTextNode('')), target)
         insert((this.targetAnchor = createTextNode('')), target)
@@ -330,14 +335,14 @@ export class TeleportFragment extends VaporFragment {
 
     // remove anchors
     if (this.targetStart) {
-      remove(this.targetStart, this.target!)
+      remove(this.targetStart, parentNode(this.targetStart)!)
       this.targetStart = undefined
-      remove(this.targetAnchor!, this.target!)
+      remove(this.targetAnchor!, parentNode(this.targetAnchor!)!)
       this.targetAnchor = undefined
     }
 
     if (this.anchor) {
-      remove(this.anchor, this.anchor.parentNode!)
+      remove(this.anchor, parentNode(this.anchor)!)
       this.anchor = undefined
     }
 
@@ -377,7 +382,7 @@ export class TeleportFragment extends VaporFragment {
     let nextNode = this.placeholder!.nextSibling!
     setCurrentHydrationNode(nextNode)
     this.mountAnchor = this.anchor = locateTeleportEndAnchor(nextNode)!
-    this.mountContainer = this.anchor.parentNode
+    this.mountContainer = parentNode(this.anchor)
     if (target) {
       this.hydrateTargetAnchors(target, targetNode)
     } else {