]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: add more hmr tests + refactor
authordaiwei <daiwei521@126.com>
Thu, 27 Mar 2025 03:42:36 +0000 (11:42 +0800)
committerdaiwei <daiwei521@126.com>
Thu, 27 Mar 2025 03:42:36 +0000 (11:42 +0800)
packages/runtime-vapor/__tests__/components/Teleport.spec.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/components/Teleport.ts
packages/runtime-vapor/src/hmr.ts
packages/runtime-vapor/src/renderEffect.ts

index d5ff414224937f7dcb8ce9bb56dd852b22535af5..6863d398bfc3e02b462e30124069b89effb0d613 100644 (file)
@@ -122,11 +122,11 @@ describe('renderer: VaporTeleport', () => {
   })
 
   describe('HMR', () => {
-    test('rerender', async () => {
+    test('rerender child + rerender parent', async () => {
       const target = document.createElement('div')
       const root = document.createElement('div')
-      const childId = 'test1-child'
-      const parentId = 'test1-parent'
+      const childId = 'test1-child-rerender'
+      const parentId = 'test1-parent-rerender'
 
       const { component: Child } = define({
         __hmrId: childId,
@@ -185,11 +185,78 @@ describe('renderer: VaporTeleport', () => {
       expect(target.innerHTML).toBe('<div>teleported 2</div>')
     })
 
-    test('reload', async () => {
+    test('parent rerender + toggle disabled', async () => {
       const target = document.createElement('div')
       const root = document.createElement('div')
-      const childId = 'test2-child'
-      const parentId = 'test2-parent'
+      const parentId = 'test3-parent-rerender'
+      const disabled = ref(true)
+
+      const Child = defineVaporComponent({
+        render() {
+          return template('<div>teleported</div>')()
+        },
+      })
+
+      const { mount, component: Parent } = define({
+        __hmrId: parentId,
+        render() {
+          const n2 = template('<div><div>root</div></div>', true)() as any
+          setInsertionState(n2, 0)
+          createComp(
+            VaporTeleport,
+            {
+              to: () => target,
+              disabled: () => disabled.value,
+            },
+            {
+              default: () => createComp(Child),
+            },
+          )
+          return n2
+        },
+      }).create()
+      createRecord(parentId, Parent as any)
+      mount(root)
+
+      expect(root.innerHTML).toBe(
+        '<div><!--teleport--><div>teleported</div><div>root</div></div>',
+      )
+      expect(target.innerHTML).toBe('')
+
+      // rerender parent
+      rerender(parentId, () => {
+        const n2 = template('<div><div>root 2</div></div>', true)() as any
+        setInsertionState(n2, 0)
+        createComp(
+          VaporTeleport,
+          {
+            to: () => target,
+            disabled: () => disabled.value,
+          },
+          {
+            default: () => createComp(Child),
+          },
+        )
+        return n2
+      })
+
+      expect(root.innerHTML).toBe(
+        '<div><!--teleport--><div>teleported</div><div>root 2</div></div>',
+      )
+      expect(target.innerHTML).toBe('')
+
+      // toggle disabled
+      disabled.value = false
+      await nextTick()
+      expect(root.innerHTML).toBe('<div><!--teleport--><div>root 2</div></div>')
+      expect(target.innerHTML).toBe('<div>teleported</div>')
+    })
+
+    test('reload child + reload parent', async () => {
+      const target = document.createElement('div')
+      const root = document.createElement('div')
+      const childId = 'test1-child-reload'
+      const parentId = 'test1-parent-reload'
 
       const { component: Child } = define({
         __hmrId: childId,
@@ -317,11 +384,11 @@ describe('renderer: VaporTeleport', () => {
       expect(target.innerHTML).toBe('')
     })
 
-    test('reload child + toggle disabled', async () => {
+    test('reload single root child + toggle disabled', async () => {
       const target = document.createElement('div')
       const root = document.createElement('div')
-      const childId = 'test3-child'
-      const parentId = 'test3-parent'
+      const childId = 'test2-child-reload'
+      const parentId = 'test2-parent-reload'
 
       const disabled = ref(true)
       const { component: Child } = define({
@@ -353,6 +420,7 @@ describe('renderer: VaporTeleport', () => {
               disabled: () => ctx.disabled,
             },
             {
+              // with single root child
               default: () => createComp(Child),
             },
           )
@@ -416,6 +484,109 @@ describe('renderer: VaporTeleport', () => {
       expect(root.innerHTML).toBe('<!--teleport--><div>root</div>')
       expect(target.innerHTML).toBe('<div>teleported 3</div>')
     })
+
+    test('reload multiple root children + toggle disabled', async () => {
+      const target = document.createElement('div')
+      const root = document.createElement('div')
+      const childId = 'test3-child-reload'
+      const parentId = 'test3-parent-reload'
+
+      const disabled = ref(true)
+      const { component: Child } = define({
+        __hmrId: childId,
+        setup() {
+          const msg = ref('teleported')
+          return { msg }
+        },
+        render(ctx) {
+          const n0 = template(`<div> </div>`)()
+          const x0 = child(n0 as any)
+          renderEffect(() => setText(x0 as any, ctx.msg))
+          return [n0]
+        },
+      })
+      createRecord(childId, Child as any)
+
+      const { mount, component: Parent } = define({
+        __hmrId: parentId,
+        setup() {
+          const msg = ref('root')
+          return { msg, disabled }
+        },
+        render(ctx) {
+          const n0 = createComp(
+            VaporTeleport,
+            {
+              to: () => target,
+              disabled: () => ctx.disabled,
+            },
+            {
+              default: () => {
+                // with multiple root children
+                return [createComp(Child), template(`<span>child</span>`)()]
+              },
+            },
+          )
+          const n1 = template(`<div> </div>`)()
+          const x0 = child(n1 as any)
+          renderEffect(() => setText(x0 as any, ctx.msg))
+          return [n0, n1]
+        },
+      }).create()
+      createRecord(parentId, Parent as any)
+      mount(root)
+
+      expect(root.innerHTML).toBe(
+        '<div>teleported</div><span>child</span><!--teleport--><div>root</div>',
+      )
+      expect(target.innerHTML).toBe('')
+
+      // reload child by changing msg
+      reload(childId, {
+        __hmrId: childId,
+        __vapor: true,
+        setup() {
+          const msg = ref('teleported 2')
+          return { msg }
+        },
+        render(ctx: any) {
+          const n0 = template(`<div> </div>`)()
+          const x0 = child(n0 as any)
+          renderEffect(() => setText(x0 as any, ctx.msg))
+          return [n0]
+        },
+      })
+      expect(root.innerHTML).toBe(
+        '<div>teleported 2</div><span>child</span><!--teleport--><div>root</div>',
+      )
+      expect(target.innerHTML).toBe('')
+
+      // reload child again by changing msg
+      reload(childId, {
+        __hmrId: childId,
+        __vapor: true,
+        setup() {
+          const msg = ref('teleported 3')
+          return { msg }
+        },
+        render(ctx: any) {
+          const n0 = template(`<div> </div>`)()
+          const x0 = child(n0 as any)
+          renderEffect(() => setText(x0 as any, ctx.msg))
+          return [n0]
+        },
+      })
+      expect(root.innerHTML).toBe(
+        '<div>teleported 3</div><span>child</span><!--teleport--><div>root</div>',
+      )
+      expect(target.innerHTML).toBe('')
+
+      // toggle disabled
+      disabled.value = false
+      await nextTick()
+      expect(root.innerHTML).toBe('<!--teleport--><div>root</div>')
+      expect(target.innerHTML).toBe('<div>teleported 3</div><span>child</span>')
+    })
   })
 })
 
index 208b2d778726b344f552bb147d0ce373955353c0..209bf135045c0ad8e81ce739aade74466a4975bf 100644 (file)
@@ -176,14 +176,6 @@ export function createComponent(
       frag.hydrate()
     }
 
-    // remove the teleport content from the parent tree for HMR updates
-    if (__DEV__) {
-      const instance = currentInstance as VaporComponentInstance
-      ;(instance!.hmrEffects || (instance!.hmrEffects = [])).push(() => {
-        frag.remove(frag.anchor.parentNode!)
-      })
-    }
-
     return frag as any
   }
 
@@ -405,8 +397,8 @@ export class VaporComponentInstance implements GenericComponentInstance {
   setupState?: Record<string, any>
   devtoolsRawSetupState?: any
   hmrRerender?: () => void
+  hmrRerenderEffects?: (() => void)[]
   hmrReload?: (newComp: VaporComponent) => void
-  hmrEffects?: (() => void)[]
   propsOptions?: NormalizedPropsOptions
   emitsOptions?: ObjectEmitsOptions | null
   isSingleRoot?: boolean
index 65fdc53e0ac16a70aee8ff3b1b044ffc13407efe..1af56e65b55ec0435807d456cfcdefb129d6d2fa 100644 (file)
@@ -1,6 +1,7 @@
 import {
   TeleportEndKey,
   type TeleportProps,
+  currentInstance,
   isTeleportDeferred,
   isTeleportDisabled,
   queuePostFlushCb,
@@ -17,7 +18,6 @@ import type {
 import { rawPropsProxyHandlers } from '../componentProps'
 import { renderEffect } from '../renderEffect'
 import { extend, isArray } from '@vue/shared'
-import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
 import { VaporFragment } from '../fragment'
 
 export const teleportStack: TeleportFragment[] = __DEV__
@@ -29,9 +29,9 @@ export const instanceToTeleportMap: WeakMap<
 > = __DEV__ ? new WeakMap() : (undefined as any)
 
 /**
- * dev only.
+ * dev only
  * when the root child component updates, synchronously update
- * the TeleportFragment's children and nodes.
+ * the TeleportFragment's nodes.
  */
 export function handleTeleportRootComponentHmrReload(
   instance: VaporComponentInstance,
@@ -41,12 +41,10 @@ export function handleTeleportRootComponentHmrReload(
   if (teleport) {
     instanceToTeleportMap.set(newInstance, teleport)
     if (teleport.nodes === instance) {
-      teleport.children = teleport.nodes = newInstance
+      teleport.nodes = newInstance
     } else if (isArray(teleport.nodes)) {
       const i = teleport.nodes.indexOf(instance)
-      if (i > -1) {
-        ;(teleport.children as Block[])[i] = teleport.nodes[i] = newInstance
-      }
+      if (i !== -1) teleport.nodes[i] = newInstance
     }
   }
 }
@@ -61,37 +59,42 @@ export const VaporTeleportImpl = {
       ? new TeleportFragment('teleport')
       : new TeleportFragment()
 
-    pauseTracking()
-    const scope = (frag.scope = new EffectScope())
-    scope!.run(() => {
-      renderEffect(() => {
-        __DEV__ && teleportStack.push(frag)
-        frag.updateChildren(
-          (frag.children = slots.default && (slots.default as BlockFn)()),
-        )
-        __DEV__ && teleportStack.pop()
-      })
+    const updateChildrenEffect = renderEffect(() => {
+      __DEV__ && teleportStack.push(frag)
+      frag.updateChildren(slots.default && (slots.default as BlockFn)())
+      __DEV__ && teleportStack.pop()
+    })
 
-      renderEffect(() => {
-        frag.update(
-          // access the props to trigger tracking
-          extend(
-            {},
-            new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps,
-          ),
-          frag.children!,
-        )
-      })
+    const updateEffect = renderEffect(() => {
+      frag.update(
+        // access the props to trigger tracking
+        extend(
+          {},
+          new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps,
+        ),
+      )
     })
-    resetTracking()
 
     if (__DEV__) {
-      // used in normalizeBlock to get the nodes of a TeleportFragment
-      // during hmr update. return empty array if the teleport content
-      // is mounted into the target container.
+      // used in `normalizeBlock` to get nodes of TeleportFragment during
+      // HMR updates. returns empty array if content is mounted in target
+      // container to prevent incorrect parent node lookup.
       frag.getNodes = () => {
         return frag.parent !== frag.currentParent ? [] : frag.nodes
       }
+
+      // for HMR re-render
+      const instance = currentInstance as VaporComponentInstance
+      ;(
+        instance!.hmrRerenderEffects || (instance!.hmrRerenderEffects = [])
+      ).push(() => {
+        // remove the teleport content
+        frag.remove(frag.anchor.parentNode!)
+
+        // stop effects
+        updateChildrenEffect.stop()
+        updateEffect.stop()
+      })
     }
 
     return frag
@@ -100,8 +103,6 @@ export const VaporTeleportImpl = {
 
 class TeleportFragment extends VaporFragment {
   anchor: Node
-  scope: EffectScope | undefined
-  children: Block | undefined
 
   private targetStart?: Node
   private mainAnchor?: Node
@@ -128,19 +129,18 @@ class TeleportFragment extends VaporFragment {
   }
 
   updateChildren(children: Block): void {
-    // not mounted yet, early return
-    if (!this.parent) return
-
-    // teardown previous children
-    remove(this.nodes, this.currentParent)
-
-    // mount new
-    insert((this.nodes = children), this.currentParent, this.currentAnchor)
+    // not mounted yet
+    if (!this.parent) {
+      this.nodes = children
+    } else {
+      // teardown previous nodes
+      remove(this.nodes, this.currentParent)
+      // mount new nodes
+      insert((this.nodes = children), this.currentParent, this.currentAnchor)
+    }
   }
 
-  update(props: TeleportProps, children: Block): void {
-    this.nodes = children
-
+  update(props: TeleportProps): void {
     const mount = (parent: ParentNode, anchor: Node | null) => {
       insert(
         this.nodes,
@@ -203,16 +203,10 @@ class TeleportFragment extends VaporFragment {
   }
 
   remove = (parent: ParentNode | undefined): void => {
-    // stop effect scope
-    if (this.scope) {
-      this.scope.stop()
-      this.scope = undefined
-    }
-
     // remove nodes
     if (this.nodes) {
       remove(this.nodes, this.currentParent)
-      this.children = this.nodes = []
+      this.nodes = []
     }
 
     // remove anchors
index d72800cf53e36510569e4621112b9ed339424e87..63e5376896a2fa63ae020aad7682a16abe487e5b 100644 (file)
@@ -20,9 +20,9 @@ export function hmrRerender(instance: VaporComponentInstance): void {
   const parent = normalized[0].parentNode!
   const anchor = normalized[normalized.length - 1].nextSibling
   remove(instance.block, parent)
-  if (instance.hmrEffects) {
-    instance.hmrEffects.forEach(e => e())
-    instance.hmrEffects.length = 0
+  if (instance.hmrRerenderEffects) {
+    instance.hmrRerenderEffects.forEach(e => e())
+    instance.hmrRerenderEffects.length = 0
   }
   const prev = currentInstance
   simpleSetCurrentInstance(instance)
@@ -41,10 +41,6 @@ export function hmrReload(
   const parent = normalized[0].parentNode!
   const anchor = normalized[normalized.length - 1].nextSibling
   unmountComponent(instance, parent)
-  if (instance.hmrEffects) {
-    instance.hmrEffects.forEach(e => e())
-    instance.hmrEffects.length = 0
-  }
   const prev = currentInstance
   simpleSetCurrentInstance(instance.parent)
   const newInstance = createComponent(
index a9fa9b33562dd9e6c9fad6348951905b772fef15..227d7933e78eac3ee45a74f7e8deb6e5ec11e2db 100644 (file)
@@ -11,7 +11,10 @@ import {
 import { type VaporComponentInstance, isVaporComponent } from './component'
 import { invokeArrayFns } from '@vue/shared'
 
-export function renderEffect(fn: () => void, noLifecycle = false): void {
+export function renderEffect(
+  fn: () => void,
+  noLifecycle = false,
+): ReactiveEffect<void> {
   const instance = currentInstance as VaporComponentInstance | null
   const scope = getCurrentScope()
   if (__DEV__ && !__TEST__ && !scope && !isVaporComponent(instance)) {
@@ -66,5 +69,6 @@ export function renderEffect(fn: () => void, noLifecycle = false): void {
   effect.scheduler = () => queueJob(job)
   effect.run()
 
+  return effect
   // TODO recurse handling
 }