diff --git a/packages/compiler-vapor/src/generators/template.ts b/packages/compiler-vapor/src/generators/template.ts index 5a066b09e..9e2b81061 100644 --- a/packages/compiler-vapor/src/generators/template.ts +++ b/packages/compiler-vapor/src/generators/template.ts @@ -24,10 +24,10 @@ export function genSelf( context: CodegenContext, ): CodeFragment[] { const [frag, push] = buildCodeFragment() - const { id, template, operation } = dynamic + const { id, template, operation, dynamicChildOffset } = dynamic if (id !== undefined && template !== undefined) { - push(NEWLINE, `const n${id} = t${template}()`) + push(NEWLINE, `const n${id} = t${template}(${dynamicChildOffset || ''})`) push(...genDirectivesForElement(id, context)) } diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index 5af12457b..0e499c040 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -266,6 +266,7 @@ export interface IRDynamicInfo { children: IRDynamicInfo[] template?: number hasDynamicChild?: boolean + dynamicChildOffset?: number operation?: OperationNode needsKey?: boolean } diff --git a/packages/compiler-vapor/src/transforms/transformChildren.ts b/packages/compiler-vapor/src/transforms/transformChildren.ts index da47438c2..1baf7a942 100644 --- a/packages/compiler-vapor/src/transforms/transformChildren.ts +++ b/packages/compiler-vapor/src/transforms/transformChildren.ts @@ -59,7 +59,7 @@ export const transformChildren: NodeTransform = (node, context) => { function processDynamicChildren(context: TransformContext) { let prevDynamics: IRDynamicInfo[] = [] - let hasStaticTemplate = false + let staticCount = 0 const children = context.dynamic.children for (const [index, child] of children.entries()) { @@ -69,7 +69,7 @@ function processDynamicChildren(context: TransformContext) { if (!(child.flags & DynamicFlag.NON_TEMPLATE)) { if (prevDynamics.length) { - if (hasStaticTemplate) { + if (staticCount) { // each dynamic child gets its own placeholder node. // this makes it easier to locate the corresponding node during hydration. for (let i = 0; i < prevDynamics.length; i++) { @@ -92,12 +92,13 @@ function processDynamicChildren(context: TransformContext) { } prevDynamics = [] } - hasStaticTemplate = true + staticCount++ } } if (prevDynamics.length) { - registerInsertion(prevDynamics, context) + registerInsertion(prevDynamics, context, undefined) + context.dynamic.dynamicChildOffset = staticCount } } diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts index fbc27f1d4..bef7a1082 100644 --- a/packages/runtime-vapor/__tests__/hydration.spec.ts +++ b/packages/runtime-vapor/__tests__/hydration.spec.ts @@ -476,6 +476,34 @@ describe('Vapor Mode hydration', () => { ) }) + test('consecutive components with insertion parent', async () => { + const data = reactive({ foo: 'foo', bar: 'bar' }) + const { container } = await testHydration( + ` + `, + { + Child1: ``, + Child2: ``, + }, + data, + ) + expect(container.innerHTML).toBe( + `
foobar
`, + ) + + data.foo = 'foo1' + data.bar = 'bar1' + await nextTick() + expect(container.innerHTML).toBe( + `
foo1bar1
`, + ) + }) + test('nested consecutive components with anchor insertion', async () => { const { container, data } = await testHydration( ` @@ -1314,6 +1342,38 @@ describe('Vapor Mode hydration', () => { ) }) + test('consecutive component with insertion parent', async () => { + const data = reactive({ + show: true, + foo: 'foo', + bar: 'bar', + }) + const { container } = await testHydration( + ``, + { + Child: ``, + Child2: ``, + }, + data, + ) + expect(container.innerHTML).toBe( + `
` + + `foo` + + `bar` + + `
` + + ``, + ) + + data.show = false + await nextTick() + expect(container.innerHTML).toBe(``) + }) + test('consecutive v-if on component with anchor insertion', async () => { const data = ref(true) const { container } = await testHydration( @@ -2354,6 +2414,31 @@ describe('Vapor Mode hydration', () => { ``, ) }) + + test('slot fallback', async () => { + const data = reactive({ + foo: 'foo', + }) + const { container } = await testHydration( + ``, + { + Child: ``, + }, + data, + ) + expect(container.innerHTML).toBe( + `foo`, + ) + + data.foo = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `bar`, + ) + }) }) describe.todo('transition', async () => { @@ -3912,6 +3997,79 @@ describe('VDOM hydration interop', () => { expect(container.innerHTML).toMatchInlineSnapshot(`"false"`) }) + test('nested components (VDOM -> Vapor -> VDOM (with slot fallback))', async () => { + const data = ref(true) + const { container } = await testHydrationInterop( + ` + `, + { + VaporChild: { + code: ``, + vapor: true, + }, + VdomChild: { + code: ` + `, + vapor: false, + }, + }, + data, + ) + + expect(container.innerHTML).toMatchInlineSnapshot( + `"true"`, + ) + + data.value = false + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot( + `"false"`, + ) + }) + + test.todo( + 'nested components (VDOM -> Vapor(with slot fallback) -> VDOM)', + async () => { + const data = ref(true) + const { container } = await testHydrationInterop( + ` + `, + { + VaporChild: { + code: ``, + vapor: true, + }, + VdomChild: { + code: ` + `, + vapor: false, + }, + }, + data, + ) + + expect(container.innerHTML).toMatchInlineSnapshot( + `"true vapor fallback"`, + ) + + data.value = false + await nextTick() + expect(container.innerHTML).toMatchInlineSnapshot( + `"false vapor fallback"`, + ) + }, + ) + test('vapor slot render vdom component', async () => { const data = ref(true) const { container } = await testHydrationInterop( diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index b6c142967..3a24ba829 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -86,7 +86,7 @@ export const createFor = ( const _insertionParent = insertionParent const _insertionAnchor = insertionAnchor if (isHydrating) { - locateHydrationNode(true) + locateHydrationNode() } else { resetInsertionState() } diff --git a/packages/runtime-vapor/src/dom/hydration.ts b/packages/runtime-vapor/src/dom/hydration.ts index e3f666b5b..b1ac013cf 100644 --- a/packages/runtime-vapor/src/dom/hydration.ts +++ b/packages/runtime-vapor/src/dom/hydration.ts @@ -6,11 +6,12 @@ import { setInsertionState, } from '../insertionState' import { + _nthChild, disableHydrationNodeLookup, enableHydrationNodeLookup, next, } from './node' -import { isVaporAnchors, isVaporFragmentAnchor } from '@vue/shared' +import { isVaporAnchors } from '@vue/shared' export let isHydrating = false export let currentHydrationNode: Node | null = null @@ -29,9 +30,9 @@ function performHydration( if (!isOptimized) { adoptTemplate = adoptTemplateImpl locateHydrationNode = locateHydrationNodeImpl - // optimize anchor cache lookup - ;(Comment.prototype as any).$fs = undefined + ;(Comment.prototype as any).$fe = undefined + ;(Node.prototype as any).$dp = undefined isOptimized = true } enableHydrationNodeLookup() @@ -58,12 +59,20 @@ export function hydrateNode(node: Node, fn: () => void): void { } export let adoptTemplate: (node: Node, template: string) => Node | null -export let locateHydrationNode: (hasFragmentAnchor?: boolean) => void +export let locateHydrationNode: () => void + +let inVNodeHydration = 0 +export function setInVNodeHydration(): void { + inVNodeHydration++ +} +export function unsetInVNodeHydration(): void { + inVNodeHydration-- +} type Anchor = Comment & { - // cached matching fragment start to avoid repeated traversal + // cached matching fragment end to avoid repeated traversal // on nested fragments - $fs?: Anchor + $fe?: Anchor } export const isComment = (node: Node, data: string): node is Anchor => @@ -99,9 +108,9 @@ function adoptTemplateImpl(node: Node, template: string): Node | null { return node } -const hydrationPositionMap = new WeakMap() +const nextHydrationNodeMap = new WeakMap() -function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) { +function locateHydrationNodeImpl() { let node: Node | null // prepend / firstChild if (insertionAnchor === 0) { @@ -113,51 +122,31 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) { node = insertionAnchor } else { node = insertionParent - ? hydrationPositionMap.get(insertionParent) || insertionParent.lastChild + ? nextHydrationNodeMap.get(insertionParent) || + (insertionParent.$dp !== undefined + ? _nthChild(insertionParent, insertionParent.$dp) + : currentHydrationNode) : currentHydrationNode - // if node is a vapor fragment anchor, find the previous one - if (hasFragmentAnchor && node && isVaporFragmentAnchor(node)) { - node = node.previousSibling - if (__DEV__ && !node) { - // this should not happen - throw new Error(`vapor fragment anchor previous node was not found.`) - } - } - - if (node && isComment(node, ']')) { - // fragment backward search - if (node.$fs) { - // already cached matching fragment start - node = node.$fs + let nextNode: Node | null = null + while (node) { + const isFragStart = isComment(node, '[') + if (isFragStart) + nextNode = locateEndAnchor(node as Anchor, '[', ']')!.nextSibling + if ( + isNonHydrationNode(node) || + // don't skip fragment start for vnode hydration + (!inVNodeHydration && isFragStart) + ) { + node = node.nextSibling! } else { - let cur: Node | null = node - let curFragEnd = node - let fragDepth = 0 - node = null - while (cur) { - cur = cur.previousSibling - if (cur) { - if (isComment(cur, '[')) { - curFragEnd.$fs = cur - if (!fragDepth) { - node = cur - break - } else { - fragDepth-- - } - } else if (isComment(cur, ']')) { - curFragEnd = cur - fragDepth++ - } - } - } + break } } if (insertionParent && node) { - const prev = node.previousSibling - if (prev) hydrationPositionMap.set(insertionParent, prev) + const next = nextNode || node.nextSibling + if (next) nextHydrationNodeMap.set(insertionParent, next) } } @@ -171,24 +160,28 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) { } export function locateEndAnchor( - node: Node | null, + node: Anchor, open = '[', close = ']', ): Node | null { - let match = 0 - while (node) { - node = node.nextSibling - if (node && node.nodeType === 8) { - if ((node as Comment).data === open) match++ - if ((node as Comment).data === close) { - if (match === 0) { - return node - } else { - match-- - } + // already cached matching end + if (node.$fe) { + return node.$fe + } + + const stack: Anchor[] = [node] + while ((node = node.nextSibling as Anchor) && stack.length > 0) { + if (node.nodeType === 8) { + if (node.data === open) { + stack.push(node) + } else if (node.data === close) { + const matchingOpen = stack.pop()! + matchingOpen.$fe = node + if (stack.length === 0) return node } } } + return null } diff --git a/packages/runtime-vapor/src/dom/template.ts b/packages/runtime-vapor/src/dom/template.ts index f6cdc3ff4..809314111 100644 --- a/packages/runtime-vapor/src/dom/template.ts +++ b/packages/runtime-vapor/src/dom/template.ts @@ -6,13 +6,16 @@ let t: HTMLTemplateElement /*! #__NO_SIDE_EFFECTS__ */ export function template(html: string, root?: boolean) { let node: Node - return (): Node & { $root?: true } => { + return (n?: number): Node & { $root?: true } => { if (isHydrating) { if (__DEV__ && !currentHydrationNode) { // TODO this should not happen throw new Error('No current hydration node') } - return adoptTemplate(currentHydrationNode!, html)! + node = adoptTemplate(currentHydrationNode!, html)! + // dynamic node position, default is 0 + ;(node as any).$dp = n || 0 + return node } // fast path for text nodes if (html[0] !== '<') { diff --git a/packages/runtime-vapor/src/fragment.ts b/packages/runtime-vapor/src/fragment.ts index 1e4328ac7..d595eb641 100644 --- a/packages/runtime-vapor/src/fragment.ts +++ b/packages/runtime-vapor/src/fragment.ts @@ -60,7 +60,7 @@ export class DynamicFragment extends VaporFragment { constructor(anchorLabel?: string) { super([]) if (isHydrating) { - locateHydrationNode(true) + locateHydrationNode() this.hydrate(anchorLabel!) } else { this.anchor = diff --git a/packages/runtime-vapor/src/insertionState.ts b/packages/runtime-vapor/src/insertionState.ts index c8c7ffbcd..5c4c41fe2 100644 --- a/packages/runtime-vapor/src/insertionState.ts +++ b/packages/runtime-vapor/src/insertionState.ts @@ -1,4 +1,16 @@ -export let insertionParent: ParentNode | undefined +export let insertionParent: + | (ParentNode & { + // dynamic node position - hydration only + // indicates the position where dynamic nodes begin within the parent + // during hydration, static nodes before this index are skipped + // + // Example: + // const t0 = _template("
", true) + // const n4 = t0(2) // n4.$dp = 2 + // The first 2 nodes are static, dynamic nodes start from index 2 + $dp?: number + }) + | undefined export let insertionAnchor: Node | 0 | undefined /** diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 5d6d9ebb2..d8a8c09a1 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -55,6 +55,9 @@ import { currentHydrationNode, isHydrating, locateHydrationNode, + setCurrentHydrationNode, + setInVNodeHydration, + unsetInVNodeHydration, hydrateNode as vaporHydrateNode, } from './dom/hydration' import { DynamicFragment, VaporFragment, isFragment } from './fragment' @@ -170,7 +173,11 @@ const vaporInteropImpl: Omit< setVaporTransitionHooks(component as any, hooks as VaporTransitionHooks) }, - hydrate: vaporHydrateNode, + hydrate(node: Node, fn: () => void) { + setInVNodeHydration() + vaporHydrateNode(node, fn) + unsetInVNodeHydration() + }, } const vaporSlotPropsProxyHandler: ProxyHandler< @@ -261,17 +268,7 @@ function createVDOMComponent( if (transition) setVNodeTransitionHooks(vnode, transition) if (isHydrating) { - ;( - vdomHydrateNode || - (vdomHydrateNode = ensureHydrationRenderer().hydrateNode!) - )( - currentHydrationNode!, - vnode, - parentInstance as any, - null, - null, - false, - ) + hydrateVNode(vnode, parentInstance as any) } else { internals.mt( vnode, @@ -344,42 +341,33 @@ function renderVDOMSlot( ensureVaporSlotFallback(children, fallback as any) isValidSlot = children.length > 0 } - if (isValidSlot) { if (isHydrating) { - locateHydrationNode(true) - ;( - vdomHydrateNode || - (vdomHydrateNode = ensureHydrationRenderer().hydrateNode!) - )( - currentHydrationNode!, + hydrateVNode(vnode!, parentComponent as any) + } else { + if (fallbackNodes) { + remove(fallbackNodes, parentNode) + fallbackNodes = undefined + } + internals.p( + oldVNode, vnode!, + parentNode, + anchor, parentComponent as any, null, + undefined, null, false, ) - } else if (fallbackNodes) { - remove(fallbackNodes, parentNode) - fallbackNodes = undefined } - internals.p( - oldVNode, - vnode!, - parentNode, - anchor, - parentComponent as any, - null, - undefined, - null, - false, - ) oldVNode = vnode! } else { // for forwarded slot without its own fallback, use the fallback // provided by the slot outlet. // re-fetch `frag.fallback` as it may have been updated at `createSlot` fallback = frag.fallback + if (fallback && !fallbackNodes) { // mount fallback if (oldVNode) { @@ -451,7 +439,12 @@ const createFallback = const frag = new VaporFragment([]) frag.insert = (parentNode, anchor) => { fallbackNodes.forEach(vnode => { - internals.p(null, vnode, parentNode, anchor, parentComponent) + // hydrate fallback + if (isHydrating) { + hydrateVNode(vnode, parentComponent as any) + } else { + internals.p(null, vnode, parentNode, anchor, parentComponent) + } }) } frag.remove = parentNode => { @@ -465,3 +458,24 @@ const createFallback = // vapor slot return fallbackNodes as Block } + +function hydrateVNode( + vnode: VNode, + parentComponent: ComponentInternalInstance | null, +) { + // keep fragment start anchor, hydrateNode uses it to + // determine if node is a fragmentStart + setInVNodeHydration() + locateHydrationNode() + if (!vdomHydrateNode) vdomHydrateNode = ensureHydrationRenderer().hydrateNode! + const nextNode = vdomHydrateNode( + currentHydrationNode!, + vnode, + parentComponent, + null, + null, + false, + ) + unsetInVNodeHydration() + setCurrentHydrationNode(nextNode) +}