From: daiwei Date: Thu, 27 Mar 2025 03:42:36 +0000 (+0800) Subject: wip: add more hmr tests + refactor X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ba6577fac1b61854ba35d21d9d5c407ea30185a1;p=thirdparty%2Fvuejs%2Fcore.git wip: add more hmr tests + refactor --- diff --git a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts index d5ff414224..6863d398bf 100644 --- a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts +++ b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts @@ -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('
teleported 2
') }) - 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('
teleported
')() + }, + }) + + const { mount, component: Parent } = define({ + __hmrId: parentId, + render() { + const n2 = template('
root
', 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( + '
teleported
root
', + ) + expect(target.innerHTML).toBe('') + + // rerender parent + rerender(parentId, () => { + const n2 = template('
root 2
', true)() as any + setInsertionState(n2, 0) + createComp( + VaporTeleport, + { + to: () => target, + disabled: () => disabled.value, + }, + { + default: () => createComp(Child), + }, + ) + return n2 + }) + + expect(root.innerHTML).toBe( + '
teleported
root 2
', + ) + expect(target.innerHTML).toBe('') + + // toggle disabled + disabled.value = false + await nextTick() + expect(root.innerHTML).toBe('
root 2
') + expect(target.innerHTML).toBe('
teleported
') + }) + + 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('
root
') expect(target.innerHTML).toBe('
teleported 3
') }) + + 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(`
`)() + 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(`child`)()] + }, + }, + ) + const n1 = template(`
`)() + 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( + '
teleported
child
root
', + ) + 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(`
`)() + const x0 = child(n0 as any) + renderEffect(() => setText(x0 as any, ctx.msg)) + return [n0] + }, + }) + expect(root.innerHTML).toBe( + '
teleported 2
child
root
', + ) + 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(`
`)() + const x0 = child(n0 as any) + renderEffect(() => setText(x0 as any, ctx.msg)) + return [n0] + }, + }) + expect(root.innerHTML).toBe( + '
teleported 3
child
root
', + ) + expect(target.innerHTML).toBe('') + + // toggle disabled + disabled.value = false + await nextTick() + expect(root.innerHTML).toBe('
root
') + expect(target.innerHTML).toBe('
teleported 3
child') + }) }) }) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 208b2d7787..209bf13504 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -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 devtoolsRawSetupState?: any hmrRerender?: () => void + hmrRerenderEffects?: (() => void)[] hmrReload?: (newComp: VaporComponent) => void - hmrEffects?: (() => void)[] propsOptions?: NormalizedPropsOptions emitsOptions?: ObjectEmitsOptions | null isSingleRoot?: boolean diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 65fdc53e0a..1af56e65b5 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -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 diff --git a/packages/runtime-vapor/src/hmr.ts b/packages/runtime-vapor/src/hmr.ts index d72800cf53..63e5376896 100644 --- a/packages/runtime-vapor/src/hmr.ts +++ b/packages/runtime-vapor/src/hmr.ts @@ -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( diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts index a9fa9b3356..227d7933e7 100644 --- a/packages/runtime-vapor/src/renderEffect.ts +++ b/packages/runtime-vapor/src/renderEffect.ts @@ -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 { 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 }