wip: fix teleport root component hmr reload

This commit is contained in:
daiwei 2025-03-26 17:30:32 +08:00
parent b6468562e6
commit b474ce0a1e
5 changed files with 62 additions and 16 deletions

View File

@ -317,7 +317,7 @@ describe('renderer: VaporTeleport', () => {
expect(target.innerHTML).toBe('') expect(target.innerHTML).toBe('')
}) })
test.todo('reload child + toggle disabled', async () => { test('reload child + toggle disabled', async () => {
const target = document.createElement('div') const target = document.createElement('div')
const root = document.createElement('div') const root = document.createElement('div')
const childId = 'test3-child' const childId = 'test3-child'
@ -410,8 +410,6 @@ describe('renderer: VaporTeleport', () => {
) )
expect(target.innerHTML).toBe('') expect(target.innerHTML).toBe('')
//bug: child reload not update teleport fragment's nodes
// toggle disabled // toggle disabled
disabled.value = false disabled.value = false
await nextTick() await nextTick()

View File

@ -25,6 +25,7 @@ export class VaporFragment {
anchor?: Node anchor?: Node
insert?: (parent: ParentNode, anchor: Node | null) => void insert?: (parent: ParentNode, anchor: Node | null) => void
remove?: (parent?: ParentNode) => void remove?: (parent?: ParentNode) => void
getNodes?: () => Block
constructor(nodes: Block) { constructor(nodes: Block) {
this.nodes = nodes this.nodes = nodes
@ -184,8 +185,8 @@ export function normalizeBlock(block: Block): Node[] {
} else if (isVaporComponent(block)) { } else if (isVaporComponent(block)) {
nodes.push(...normalizeBlock(block.block!)) nodes.push(...normalizeBlock(block.block!))
} else { } else {
if ((block as any).getNodes) { if (block.getNodes) {
nodes.push(...normalizeBlock((block as any).getNodes())) nodes.push(...normalizeBlock(block.getNodes()))
} else { } else {
nodes.push(...normalizeBlock(block.nodes)) nodes.push(...normalizeBlock(block.nodes))
} }

View File

@ -60,7 +60,11 @@ import {
import { hmrReload, hmrRerender } from './hmr' import { hmrReload, hmrRerender } from './hmr'
import { isHydrating, locateHydrationNode } from './dom/hydration' import { isHydrating, locateHydrationNode } from './dom/hydration'
import { insertionAnchor, insertionParent } from './insertionState' import { insertionAnchor, insertionParent } from './insertionState'
import type { VaporTeleportImpl } from './components/Teleport' import {
type VaporTeleportImpl,
instanceToTeleportMap,
teleportStack,
} from './components/Teleport'
export { currentInstance } from '@vue/runtime-dom' export { currentInstance } from '@vue/runtime-dom'
@ -209,6 +213,11 @@ export function createComponent(
) )
if (__DEV__) { if (__DEV__) {
let teleport = teleportStack[teleportStack.length - 1]
if (teleport) {
instanceToTeleportMap.set(instance, teleport)
}
pushWarningContext(instance) pushWarningContext(instance)
startMeasure(instance, `init`) startMeasure(instance, `init`)
@ -296,7 +305,7 @@ export function createComponent(
onScopeDispose(() => unmountComponent(instance), true) onScopeDispose(() => unmountComponent(instance), true)
if (!isHydrating && _insertionParent) { if (!isHydrating && _insertionParent) {
insert(instance.block, _insertionParent, _insertionAnchor) mountComponent(instance, _insertionParent, _insertionAnchor)
} }
return instance return instance

View File

@ -15,12 +15,45 @@ import {
remove, remove,
} from '../block' } from '../block'
import { createComment, createTextNode, querySelector } from '../dom/node' import { createComment, createTextNode, querySelector } from '../dom/node'
import type { LooseRawProps, LooseRawSlots } from '../component' import type {
LooseRawProps,
LooseRawSlots,
VaporComponentInstance,
} from '../component'
import { rawPropsProxyHandlers } from '../componentProps' import { rawPropsProxyHandlers } from '../componentProps'
import { renderEffect } from '../renderEffect' import { renderEffect } from '../renderEffect'
import { extend } from '@vue/shared' import { extend, isArray } from '@vue/shared'
import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
export const teleportStack: TeleportFragment[] = []
export const instanceToTeleportMap: WeakMap<
VaporComponentInstance,
TeleportFragment
> = __DEV__ ? new WeakMap() : (null as any)
/**
* dev only.
* when the **root** child component updates, synchronously update
* the TeleportFragment's children and nodes.
*/
export function handleTeleportChildrenHmrReload(
instance: VaporComponentInstance,
newInstance: VaporComponentInstance,
): void {
const teleport = instanceToTeleportMap.get(instance)
if (teleport) {
instanceToTeleportMap.set(newInstance, teleport)
if (teleport.nodes === instance) {
teleport.children = 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
}
}
}
}
export const VaporTeleportImpl = { export const VaporTeleportImpl = {
name: 'VaporTeleport', name: 'VaporTeleport',
__isTeleport: true, __isTeleport: true,
@ -34,11 +67,12 @@ export const VaporTeleportImpl = {
pauseTracking() pauseTracking()
const scope = (frag.scope = new EffectScope()) const scope = (frag.scope = new EffectScope())
scope!.run(() => { scope!.run(() => {
let children: Block
renderEffect(() => { renderEffect(() => {
teleportStack.push(frag)
frag.updateChildren( frag.updateChildren(
(children = slots.default && (slots.default as BlockFn)()), (frag.children = slots.default && (slots.default as BlockFn)()),
) )
teleportStack.pop()
}) })
renderEffect(() => { renderEffect(() => {
@ -48,15 +82,17 @@ export const VaporTeleportImpl = {
{}, {},
new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps, new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps,
), ),
children!, frag.children!,
) )
}) })
}) })
resetTracking() resetTracking()
if (__DEV__) { if (__DEV__) {
// TODO // used in normalizeBlock to get the nodes of a TeleportFragment
;(frag as any).getNodes = () => { // during hmr update. return empty array if the teleport content
// is mounted into the target container.
frag.getNodes = () => {
return frag.parent !== frag.currentParent ? [] : frag.nodes return frag.parent !== frag.currentParent ? [] : frag.nodes
} }
} }
@ -68,6 +104,7 @@ export const VaporTeleportImpl = {
class TeleportFragment extends VaporFragment { class TeleportFragment extends VaporFragment {
anchor: Node anchor: Node
scope: EffectScope | undefined scope: EffectScope | undefined
children: Block | undefined
private targetStart?: Node private targetStart?: Node
private mainAnchor?: Node private mainAnchor?: Node
@ -178,7 +215,7 @@ class TeleportFragment extends VaporFragment {
// remove nodes // remove nodes
if (this.nodes) { if (this.nodes) {
remove(this.nodes, this.currentParent) remove(this.nodes, this.currentParent)
this.nodes = [] this.children = this.nodes = []
} }
// remove anchors // remove anchors

View File

@ -13,6 +13,7 @@ import {
mountComponent, mountComponent,
unmountComponent, unmountComponent,
} from './component' } from './component'
import { handleTeleportChildrenHmrReload } from './components/Teleport'
export function hmrRerender(instance: VaporComponentInstance): void { export function hmrRerender(instance: VaporComponentInstance): void {
const normalized = normalizeBlock(instance.block) const normalized = normalizeBlock(instance.block)
@ -54,5 +55,5 @@ export function hmrReload(
) )
simpleSetCurrentInstance(prev, instance.parent) simpleSetCurrentInstance(prev, instance.parent)
mountComponent(newInstance, parent, anchor) mountComponent(newInstance, parent, anchor)
instance.block = newInstance.block handleTeleportChildrenHmrReload(instance, newInstance)
} }