wip: refactor hydration for v-if
This commit is contained in:
parent
38d4889de7
commit
e9c9e4903d
|
@ -245,6 +245,7 @@ describe('ssr: components', () => {
|
||||||
_push(\`<span\${_scopeId}></span>\`)
|
_push(\`<span\${_scopeId}></span>\`)
|
||||||
})
|
})
|
||||||
_push(\`<!--]--></div>\`)
|
_push(\`<!--]--></div>\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
@ -268,6 +269,7 @@ describe('ssr: components', () => {
|
||||||
_push(\`<span\${_scopeId}></span>\`)
|
_push(\`<span\${_scopeId}></span>\`)
|
||||||
})
|
})
|
||||||
_push(\`<!--]--></div>\`)
|
_push(\`<!--]--></div>\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
@ -361,6 +363,7 @@ describe('ssr: components', () => {
|
||||||
_push(\`\`)
|
_push(\`\`)
|
||||||
if (false) {
|
if (false) {
|
||||||
_push(\`<div\${_scopeId}></div>\`)
|
_push(\`<div\${_scopeId}></div>\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ describe('ssr: attrs fallthrough', () => {
|
||||||
_push(\`<!--[-->\`)
|
_push(\`<!--[-->\`)
|
||||||
if (true) {
|
if (true) {
|
||||||
_push(\`<div></div>\`)
|
_push(\`<div></div>\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,7 @@ describe('ssr: inject <style vars>', () => {
|
||||||
const _cssVars = { style: { color: _ctx.color }}
|
const _cssVars = { style: { color: _ctx.color }}
|
||||||
if (_ctx.ok) {
|
if (_ctx.ok) {
|
||||||
_push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`)
|
_push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!--[--><div\${
|
_push(\`<!--[--><div\${
|
||||||
_ssrRenderAttrs(_cssVars)
|
_ssrRenderAttrs(_cssVars)
|
||||||
|
|
|
@ -153,6 +153,7 @@ describe('ssr: <slot>', () => {
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||||
if (true) {
|
if (true) {
|
||||||
_ssrRenderSlotInner(_ctx.$slots, "default", {}, null, _push, _parent, null, true)
|
_ssrRenderSlotInner(_ctx.$slots, "default", {}, null, _push, _parent, null, true)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ describe('transition-group', () => {
|
||||||
})
|
})
|
||||||
if (false) {
|
if (false) {
|
||||||
_push(\`<div></div>\`)
|
_push(\`<div></div>\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
}
|
}
|
||||||
_push(\`</ul>\`)
|
_push(\`</ul>\`)
|
||||||
}"
|
}"
|
||||||
|
@ -123,6 +124,7 @@ describe('transition-group', () => {
|
||||||
})
|
})
|
||||||
if (_ctx.ok) {
|
if (_ctx.ok) {
|
||||||
_push(\`<div>ok</div>\`)
|
_push(\`<div>ok</div>\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
}
|
}
|
||||||
_push(\`<!--]-->\`)
|
_push(\`<!--]-->\`)
|
||||||
}"
|
}"
|
||||||
|
|
|
@ -8,6 +8,7 @@ describe('ssr: v-if', () => {
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||||
if (_ctx.foo) {
|
if (_ctx.foo) {
|
||||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
@ -23,6 +24,7 @@ describe('ssr: v-if', () => {
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||||
if (_ctx.foo) {
|
if (_ctx.foo) {
|
||||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}>hello<span>ok</span></div>\`)
|
_push(\`<div\${_ssrRenderAttrs(_attrs)}>hello<span>ok</span></div>\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
@ -38,6 +40,7 @@ describe('ssr: v-if', () => {
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||||
if (_ctx.foo) {
|
if (_ctx.foo) {
|
||||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
|
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
|
||||||
}
|
}
|
||||||
|
@ -53,8 +56,10 @@ describe('ssr: v-if', () => {
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||||
if (_ctx.foo) {
|
if (_ctx.foo) {
|
||||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else if (_ctx.bar) {
|
} else if (_ctx.bar) {
|
||||||
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
|
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
@ -70,8 +75,10 @@ describe('ssr: v-if', () => {
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||||
if (_ctx.foo) {
|
if (_ctx.foo) {
|
||||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else if (_ctx.bar) {
|
} else if (_ctx.bar) {
|
||||||
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
|
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`)
|
_push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`)
|
||||||
}
|
}
|
||||||
|
@ -82,15 +89,16 @@ describe('ssr: v-if', () => {
|
||||||
test('<template v-if> (text)', () => {
|
test('<template v-if> (text)', () => {
|
||||||
expect(compile(`<template v-if="foo">hello</template>`).code)
|
expect(compile(`<template v-if="foo">hello</template>`).code)
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"
|
"
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||||
if (_ctx.foo) {
|
if (_ctx.foo) {
|
||||||
_push(\`<!--[-->hello<!--]-->\`)
|
_push(\`<!--[-->hello<!--]-->\`)
|
||||||
} else {
|
_push(\`<!--$-->\`)
|
||||||
_push(\`<!---->\`)
|
} else {
|
||||||
}
|
_push(\`<!---->\`)
|
||||||
}"
|
}
|
||||||
`)
|
}"
|
||||||
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('<template v-if> (single element)', () => {
|
test('<template v-if> (single element)', () => {
|
||||||
|
@ -102,6 +110,7 @@ describe('ssr: v-if', () => {
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||||
if (_ctx.foo) {
|
if (_ctx.foo) {
|
||||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}>hi</div>\`)
|
_push(\`<div\${_ssrRenderAttrs(_attrs)}>hi</div>\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
@ -118,6 +127,7 @@ describe('ssr: v-if', () => {
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||||
if (_ctx.foo) {
|
if (_ctx.foo) {
|
||||||
_push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
|
_push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
@ -138,6 +148,7 @@ describe('ssr: v-if', () => {
|
||||||
_push(\`<div></div>\`)
|
_push(\`<div></div>\`)
|
||||||
})
|
})
|
||||||
_push(\`<!--]-->\`)
|
_push(\`<!--]-->\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
@ -156,6 +167,7 @@ describe('ssr: v-if', () => {
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||||
if (_ctx.foo) {
|
if (_ctx.foo) {
|
||||||
_push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
|
_push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,6 +91,7 @@ describe('ssr: v-model', () => {
|
||||||
? _ssrLooseContain(_ctx.model, _ctx.i)
|
? _ssrLooseContain(_ctx.model, _ctx.i)
|
||||||
: _ssrLooseEqual(_ctx.model, _ctx.i))) ? " selected" : ""
|
: _ssrLooseEqual(_ctx.model, _ctx.i))) ? " selected" : ""
|
||||||
}></option>\`)
|
}></option>\`)
|
||||||
|
_push(\`<!--$-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -295,8 +295,13 @@ function processChildrenDynamicInfo(
|
||||||
|
|
||||||
for (let i = 0; i < filteredChildren.length; i++) {
|
for (let i = 0; i < filteredChildren.length; i++) {
|
||||||
const child = filteredChildren[i]
|
const child = filteredChildren[i]
|
||||||
if (isStaticChildNode(child)) continue
|
if (
|
||||||
|
isStaticChildNode(child) ||
|
||||||
|
// v-if has an anchor, which can be used to distinguish the boundary
|
||||||
|
child.type === NodeTypes.IF
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
child._ssrDynamicInfo = {
|
child._ssrDynamicInfo = {
|
||||||
hasStaticPrevious: false,
|
hasStaticPrevious: false,
|
||||||
hasStaticNext: false,
|
hasStaticNext: false,
|
||||||
|
|
|
@ -74,5 +74,13 @@ function processIfBranch(
|
||||||
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
|
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
|
||||||
// optimize away nested fragments when the only child is a ForNode
|
// optimize away nested fragments when the only child is a ForNode
|
||||||
!(children.length === 1 && children[0].type === NodeTypes.FOR)
|
!(children.length === 1 && children[0].type === NodeTypes.FOR)
|
||||||
return processChildrenAsStatement(branch, context, needFragmentWrapper)
|
const statement = processChildrenAsStatement(
|
||||||
|
branch,
|
||||||
|
context,
|
||||||
|
needFragmentWrapper,
|
||||||
|
)
|
||||||
|
if (branch.condition) {
|
||||||
|
statement.body.push(createCallExpression(`_push`, ['`<!--$-->`']))
|
||||||
|
}
|
||||||
|
return statement
|
||||||
}
|
}
|
||||||
|
|
|
@ -598,14 +598,14 @@ describe('SSR hydration', () => {
|
||||||
const ctx: SSRContext = {}
|
const ctx: SSRContext = {}
|
||||||
container.innerHTML = await renderToString(h(App), ctx)
|
container.innerHTML = await renderToString(h(App), ctx)
|
||||||
expect(container.innerHTML).toBe(
|
expect(container.innerHTML).toBe(
|
||||||
'<div><!--teleport start--><!--teleport end--></div>',
|
'<div><!--teleport start--><!--teleport end--><!--$--></div>',
|
||||||
)
|
)
|
||||||
teleportContainer.innerHTML = ctx.teleports!['#target']
|
teleportContainer.innerHTML = ctx.teleports!['#target']
|
||||||
|
|
||||||
// hydrate
|
// hydrate
|
||||||
createSSRApp(App).mount(container)
|
createSSRApp(App).mount(container)
|
||||||
expect(container.innerHTML).toBe(
|
expect(container.innerHTML).toBe(
|
||||||
'<div><!--teleport start--><!--teleport end--></div>',
|
'<div><!--teleport start--><!--teleport end--><!--$--></div>',
|
||||||
)
|
)
|
||||||
expect(teleportContainer.innerHTML).toBe(
|
expect(teleportContainer.innerHTML).toBe(
|
||||||
'<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
|
'<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
|
||||||
|
@ -614,7 +614,7 @@ describe('SSR hydration', () => {
|
||||||
|
|
||||||
toggle.value = false
|
toggle.value = false
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
|
expect(container.innerHTML).toBe('<div><div>Comp2</div><!--$--></div>')
|
||||||
expect(teleportContainer.innerHTML).toBe('')
|
expect(teleportContainer.innerHTML).toBe('')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -657,21 +657,21 @@ describe('SSR hydration', () => {
|
||||||
// server render
|
// server render
|
||||||
container.innerHTML = await renderToString(h(App))
|
container.innerHTML = await renderToString(h(App))
|
||||||
expect(container.innerHTML).toBe(
|
expect(container.innerHTML).toBe(
|
||||||
'<div><!--teleport start--><!--teleport end--></div>',
|
'<div><!--teleport start--><!--teleport end--><!--$--></div>',
|
||||||
)
|
)
|
||||||
expect(teleportContainer.innerHTML).toBe('')
|
expect(teleportContainer.innerHTML).toBe('')
|
||||||
|
|
||||||
// hydrate
|
// hydrate
|
||||||
createSSRApp(App).mount(container)
|
createSSRApp(App).mount(container)
|
||||||
expect(container.innerHTML).toBe(
|
expect(container.innerHTML).toBe(
|
||||||
'<div><!--teleport start--><!--teleport end--></div>',
|
'<div><!--teleport start--><!--teleport end--><!--$--></div>',
|
||||||
)
|
)
|
||||||
expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>')
|
expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>')
|
||||||
expect(`Hydration children mismatch`).toHaveBeenWarned()
|
expect(`Hydration children mismatch`).toHaveBeenWarned()
|
||||||
|
|
||||||
toggle.value = false
|
toggle.value = false
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
|
expect(container.innerHTML).toBe('<div><div>Comp2</div><!--$--></div>')
|
||||||
expect(teleportContainer.innerHTML).toBe('')
|
expect(teleportContainer.innerHTML).toBe('')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -84,10 +84,14 @@ const getContainerType = (
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDynamicAnchor(node: Node): boolean {
|
export function isDynamicAnchor(node: Node): node is Comment {
|
||||||
return isComment(node) && (node.data === '[[' || node.data === ']]')
|
return isComment(node) && (node.data === '[[' || node.data === ']]')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isDynamicFragmentEndAnchor(node: Node): node is Comment {
|
||||||
|
return isComment(node) && node.data === '$'
|
||||||
|
}
|
||||||
|
|
||||||
export const isComment = (node: Node): node is Comment =>
|
export const isComment = (node: Node): node is Comment =>
|
||||||
node.nodeType === DOMNodeTypes.COMMENT
|
node.nodeType === DOMNodeTypes.COMMENT
|
||||||
|
|
||||||
|
@ -125,8 +129,10 @@ export function createHydrationFunctions(
|
||||||
|
|
||||||
function nextSibling(node: Node) {
|
function nextSibling(node: Node) {
|
||||||
let n = next(node)
|
let n = next(node)
|
||||||
// skip dynamic anchors
|
// skip if:
|
||||||
if (n && isDynamicAnchor(n)) {
|
// - dynamic anchors (`<!--[-->`, `<!--]-->`)
|
||||||
|
// - dynamic fragment end anchors (`<!--$-->`)
|
||||||
|
if (n && (isDynamicAnchor(n) || isDynamicFragmentEndAnchor(n))) {
|
||||||
n = next(n)
|
n = next(n)
|
||||||
}
|
}
|
||||||
return n
|
return n
|
||||||
|
@ -158,7 +164,9 @@ export function createHydrationFunctions(
|
||||||
slotScopeIds: string[] | null,
|
slotScopeIds: string[] | null,
|
||||||
optimized = false,
|
optimized = false,
|
||||||
): Node | null => {
|
): Node | null => {
|
||||||
if (isDynamicAnchor(node)) node = nextSibling(node)!
|
if (isDynamicAnchor(node) || isDynamicFragmentEndAnchor(node)) {
|
||||||
|
node = nextSibling(node)!
|
||||||
|
}
|
||||||
optimized = optimized || !!vnode.dynamicChildren
|
optimized = optimized || !!vnode.dynamicChildren
|
||||||
const isFragmentStart = isComment(node) && node.data === '['
|
const isFragmentStart = isComment(node) && node.data === '['
|
||||||
const onMismatch = () =>
|
const onMismatch = () =>
|
||||||
|
|
|
@ -560,4 +560,4 @@ export { initFeatureFlags } from './featureFlags'
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export { isDynamicAnchor } from './hydration'
|
export { isDynamicAnchor, isDynamicFragmentEndAnchor } from './hydration'
|
||||||
|
|
|
@ -730,7 +730,97 @@ describe('Vapor Mode hydration', () => {
|
||||||
expect(container.innerHTML).toMatchInlineSnapshot(`"<!--if-->"`)
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<!--if-->"`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test.todo('on component', async () => {})
|
test('on component', async () => {
|
||||||
|
const data = ref(true)
|
||||||
|
const { container } = await testHydration(
|
||||||
|
`<template>
|
||||||
|
<components.Child v-if="data"/>
|
||||||
|
</template>`,
|
||||||
|
{ Child: `<template>foo</template>` },
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"foo<!--if-->"`)
|
||||||
|
|
||||||
|
data.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(`"<!--if-->"`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('on component with anchor insertion', async () => {
|
||||||
|
const data = ref(true)
|
||||||
|
const { container } = await testHydration(
|
||||||
|
`<template>
|
||||||
|
<div>
|
||||||
|
<span/>
|
||||||
|
<components.Child v-if="data"/>
|
||||||
|
<span/>
|
||||||
|
</div>
|
||||||
|
</template>`,
|
||||||
|
{ Child: `<template>foo</template>` },
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||||
|
`"<div><span></span>foo<!--if--><span></span></div>"`,
|
||||||
|
)
|
||||||
|
|
||||||
|
data.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||||
|
`"<div><span></span><!--if--><span></span></div>"`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('consecutive v-if on component with anchor insertion', async () => {
|
||||||
|
const data = ref(true)
|
||||||
|
const { container } = await testHydration(
|
||||||
|
`<template>
|
||||||
|
<div>
|
||||||
|
<span/>
|
||||||
|
<components.Child v-if="data"/>
|
||||||
|
<components.Child v-if="data"/>
|
||||||
|
<span/>
|
||||||
|
</div>
|
||||||
|
</template>`,
|
||||||
|
{ Child: `<template>foo</template>` },
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||||
|
`"<div><span></span>foo<!--if-->foo<!--if--><span></span></div>"`,
|
||||||
|
)
|
||||||
|
|
||||||
|
data.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||||
|
`"<div><span></span><!--if--><!--if--><span></span></div>"`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('consecutive v-if on fragment component with anchor insertion', async () => {
|
||||||
|
const data = ref(true)
|
||||||
|
const { container } = await testHydration(
|
||||||
|
`<template>
|
||||||
|
<div>
|
||||||
|
<span/>
|
||||||
|
<components.Child v-if="data"/>
|
||||||
|
<components.Child v-if="data"/>
|
||||||
|
<span/>
|
||||||
|
</div>
|
||||||
|
</template>`,
|
||||||
|
{
|
||||||
|
Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||||
|
`"<div><span></span><!--[--><div>true</div>-true-<!--]--><!--if--><!--[--><div>true</div>-true-<!--]--><!--if--><span></span></div>"`,
|
||||||
|
)
|
||||||
|
|
||||||
|
data.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||||
|
`"<div><span></span><!--[--><!--]--><!--if--><!--[--><!--]--><!--if--><span></span></div>"`,
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test.todo('for')
|
test.todo('for')
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
import { type Block, type BlockFn, DynamicFragment, insert } from './block'
|
import { type Block, type BlockFn, DynamicFragment, insert } from './block'
|
||||||
import {
|
import { isHydrating, locateHydrationNode } from './dom/hydration'
|
||||||
currentHydrationNode,
|
|
||||||
isComment,
|
|
||||||
isHydrating,
|
|
||||||
locateHydrationNode,
|
|
||||||
} from './dom/hydration'
|
|
||||||
import { insertionAnchor, insertionParent } from './insertionState'
|
import { insertionAnchor, insertionParent } from './insertionState'
|
||||||
import { renderEffect } from './renderEffect'
|
import { renderEffect } from './renderEffect'
|
||||||
|
|
||||||
|
@ -16,10 +11,8 @@ export function createIf(
|
||||||
): Block {
|
): Block {
|
||||||
const _insertionParent = insertionParent
|
const _insertionParent = insertionParent
|
||||||
const _insertionAnchor = insertionAnchor
|
const _insertionAnchor = insertionAnchor
|
||||||
let _currentHydrationNode
|
|
||||||
if (isHydrating) {
|
if (isHydrating) {
|
||||||
locateHydrationNode()
|
locateHydrationNode(true)
|
||||||
_currentHydrationNode = currentHydrationNode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let frag: Block
|
let frag: Block
|
||||||
|
@ -34,23 +27,5 @@ export function createIf(
|
||||||
insert(frag, _insertionParent, _insertionAnchor)
|
insert(frag, _insertionParent, _insertionAnchor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the current hydration node is a comment, use it as an anchor
|
|
||||||
// otherwise need to insert the anchor node
|
|
||||||
// OR adjust ssr output to add anchor for v-if
|
|
||||||
else if (isHydrating && _currentHydrationNode) {
|
|
||||||
const parentNode = _currentHydrationNode.parentNode
|
|
||||||
if (parentNode) {
|
|
||||||
if (isComment(_currentHydrationNode, '')) {
|
|
||||||
if (__DEV__) _currentHydrationNode.data = 'if'
|
|
||||||
;(frag as DynamicFragment).anchor = _currentHydrationNode
|
|
||||||
} else {
|
|
||||||
parentNode.insertBefore(
|
|
||||||
(frag as DynamicFragment).anchor,
|
|
||||||
_currentHydrationNode.nextSibling,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return frag
|
return frag
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,10 @@ import {
|
||||||
mountComponent,
|
mountComponent,
|
||||||
unmountComponent,
|
unmountComponent,
|
||||||
} from './component'
|
} from './component'
|
||||||
import { createComment, createTextNode } from './dom/node'
|
import { createComment, createTextNode, next } from './dom/node'
|
||||||
import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
|
import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
|
||||||
import { isHydrating } from './dom/hydration'
|
import { currentHydrationNode, isComment, isHydrating } from './dom/hydration'
|
||||||
|
import { isDynamicFragmentEndAnchor, warn } from '@vue/runtime-dom'
|
||||||
|
|
||||||
export type Block =
|
export type Block =
|
||||||
| Node
|
| Node
|
||||||
|
@ -30,15 +31,19 @@ export class VaporFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DynamicFragment extends VaporFragment {
|
export class DynamicFragment extends VaporFragment {
|
||||||
anchor: Node
|
anchor!: Node
|
||||||
scope: EffectScope | undefined
|
scope: EffectScope | undefined
|
||||||
current?: BlockFn
|
current?: BlockFn
|
||||||
fallback?: BlockFn
|
fallback?: BlockFn
|
||||||
|
|
||||||
constructor(anchorLabel?: string) {
|
constructor(anchorLabel?: string) {
|
||||||
super([])
|
super([])
|
||||||
this.anchor =
|
if (isHydrating) {
|
||||||
__DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
|
this.hydrate(anchorLabel)
|
||||||
|
} else {
|
||||||
|
this.anchor =
|
||||||
|
__DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update(render?: BlockFn, key: any = render): void {
|
update(render?: BlockFn, key: any = render): void {
|
||||||
|
@ -75,6 +80,24 @@ export class DynamicFragment extends VaporFragment {
|
||||||
|
|
||||||
resetTracking()
|
resetTracking()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hydrate(label?: string): void {
|
||||||
|
// for v-if="false" the hydrationNode will be a empty comment node
|
||||||
|
// use it as anchor.
|
||||||
|
// otherwise, use the next sibling comment node as anchor
|
||||||
|
if (isComment(currentHydrationNode!, '')) {
|
||||||
|
this.anchor = currentHydrationNode
|
||||||
|
} else {
|
||||||
|
const anchor = next(currentHydrationNode!)
|
||||||
|
if (isDynamicFragmentEndAnchor(anchor)) {
|
||||||
|
this.anchor = anchor
|
||||||
|
} else if (__DEV__) {
|
||||||
|
// TODO warning
|
||||||
|
warn(`DynamicFragment anchor not found...`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (__DEV__ && label) (this.anchor as Comment).data = label
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFragment(val: NonNullable<unknown>): val is VaporFragment {
|
export function isFragment(val: NonNullable<unknown>): val is VaporFragment {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { warn } from '@vue/runtime-dom'
|
import { isDynamicFragmentEndAnchor, warn } from '@vue/runtime-dom'
|
||||||
import {
|
import {
|
||||||
insertionAnchor,
|
insertionAnchor,
|
||||||
insertionParent,
|
insertionParent,
|
||||||
|
@ -10,6 +10,7 @@ import {
|
||||||
disableHydrationNodeLookup,
|
disableHydrationNodeLookup,
|
||||||
enableHydrationNodeLookup,
|
enableHydrationNodeLookup,
|
||||||
next,
|
next,
|
||||||
|
prev,
|
||||||
} from './node'
|
} from './node'
|
||||||
|
|
||||||
export let isHydrating = false
|
export let isHydrating = false
|
||||||
|
@ -41,7 +42,7 @@ export function withHydration(container: ParentNode, fn: () => void): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
export let adoptTemplate: (node: Node, template: string) => Node | null
|
export let adoptTemplate: (node: Node, template: string) => Node | null
|
||||||
export let locateHydrationNode: () => void
|
export let locateHydrationNode: (isFragment?: boolean) => void
|
||||||
|
|
||||||
type Anchor = Comment & {
|
type Anchor = Comment & {
|
||||||
// cached matching fragment start to avoid repeated traversal
|
// cached matching fragment start to avoid repeated traversal
|
||||||
|
@ -82,7 +83,7 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
function locateHydrationNodeImpl() {
|
function locateHydrationNodeImpl(isFragment?: boolean) {
|
||||||
let node: Node | null
|
let node: Node | null
|
||||||
// prepend / firstChild
|
// prepend / firstChild
|
||||||
if (insertionAnchor === 0) {
|
if (insertionAnchor === 0) {
|
||||||
|
@ -92,6 +93,14 @@ function locateHydrationNodeImpl() {
|
||||||
node = insertionAnchor
|
node = insertionAnchor
|
||||||
} else {
|
} else {
|
||||||
node = insertionParent ? insertionParent.lastChild : currentHydrationNode
|
node = insertionParent ? insertionParent.lastChild : currentHydrationNode
|
||||||
|
|
||||||
|
// if the last child is a comment, it is the anchor for the fragment
|
||||||
|
// so it need to find the previous node
|
||||||
|
if (isFragment && node && isDynamicFragmentEndAnchor(node)) {
|
||||||
|
let previous = prev(node)
|
||||||
|
if (previous) node = previous
|
||||||
|
}
|
||||||
|
|
||||||
if (node && isComment(node, ']')) {
|
if (node && isComment(node, ']')) {
|
||||||
// fragment backward search
|
// fragment backward search
|
||||||
if (node.$fs) {
|
if (node.$fs) {
|
||||||
|
@ -157,3 +166,25 @@ export function locateEndAnchor(
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function locateStartAnchor(
|
||||||
|
node: Node | null,
|
||||||
|
open = '[',
|
||||||
|
close = ']',
|
||||||
|
): Node | null {
|
||||||
|
let match = 0
|
||||||
|
while (node) {
|
||||||
|
if (node.nodeType === 8) {
|
||||||
|
if ((node as Comment).data === close) match++
|
||||||
|
if ((node as Comment).data === open) {
|
||||||
|
if (match === 0) {
|
||||||
|
return node
|
||||||
|
} else {
|
||||||
|
match--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node = node.previousSibling
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import { isDynamicAnchor } from '@vue/runtime-dom'
|
import { isDynamicAnchor } from '@vue/runtime-dom'
|
||||||
import { isComment, isEmptyText, locateEndAnchor } from './hydration'
|
import {
|
||||||
|
isComment,
|
||||||
|
isEmptyText,
|
||||||
|
locateEndAnchor,
|
||||||
|
locateStartAnchor,
|
||||||
|
} from './hydration'
|
||||||
|
|
||||||
/*! #__NO_SIDE_EFFECTS__ */
|
/*! #__NO_SIDE_EFFECTS__ */
|
||||||
export function createTextNode(value = ''): Text {
|
export function createTextNode(value = ''): Text {
|
||||||
|
@ -43,21 +48,17 @@ function _next(node: Node): Node {
|
||||||
/*! #__NO_SIDE_EFFECTS__ */
|
/*! #__NO_SIDE_EFFECTS__ */
|
||||||
function __next(node: Node): Node {
|
function __next(node: Node): Node {
|
||||||
// treat dynamic node (<!--[[-->...<!--]]-->) as a single node
|
// treat dynamic node (<!--[[-->...<!--]]-->) as a single node
|
||||||
if (node && isComment(node, '[[')) {
|
if (isComment(node, '[[')) {
|
||||||
node = locateEndAnchor(node, '[[', ']]')!
|
node = locateEndAnchor(node, '[[', ']]')!
|
||||||
}
|
}
|
||||||
|
|
||||||
// treat dynamic node (<!--[-->...<!--]-->) as a single node
|
// treat dynamic node (<!--[-->...<!--]-->) as a single node
|
||||||
else if (node && isComment(node, '[')) {
|
else if (isComment(node, '[')) {
|
||||||
node = locateEndAnchor(node)!
|
node = locateEndAnchor(node)!
|
||||||
}
|
}
|
||||||
|
|
||||||
let n = node.nextSibling!
|
let n = node.nextSibling!
|
||||||
// skip if:
|
while (n && isNonHydrationNode(n)) {
|
||||||
// - dynamic anchors (<!--[[-->, <!--]]-->)
|
|
||||||
// - fragment end anchor (`<!--]-->`)
|
|
||||||
// - empty text nodes
|
|
||||||
while (n && (isDynamicAnchor(n) || isComment(n, ']') || isEmptyText(n))) {
|
|
||||||
n = n.nextSibling!
|
n = n.nextSibling!
|
||||||
}
|
}
|
||||||
return n
|
return n
|
||||||
|
@ -105,3 +106,48 @@ export function disableHydrationNodeLookup(): void {
|
||||||
next.impl = _next
|
next.impl = _next
|
||||||
nthChild.impl = _nthChild
|
nthChild.impl = _nthChild
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*! #__NO_SIDE_EFFECTS__ */
|
||||||
|
export function prev(node: Node): Node | null {
|
||||||
|
// treat dynamic node (<!--[[-->...<!--]]-->) as a single node
|
||||||
|
if (isComment(node, ']]')) {
|
||||||
|
node = locateStartAnchor(node, '[[', ']]')!
|
||||||
|
}
|
||||||
|
|
||||||
|
// treat dynamic node (<!--[-->...<!--]-->) as a single node
|
||||||
|
else if (isComment(node, ']')) {
|
||||||
|
node = locateStartAnchor(node)!
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = node.previousSibling
|
||||||
|
while (n && isNonHydrationNode(n)) {
|
||||||
|
n = n.previousSibling
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNonHydrationNode(node: Node) {
|
||||||
|
return (
|
||||||
|
// empty text nodes, no need to hydrate
|
||||||
|
isEmptyText(node) ||
|
||||||
|
// dynamic anchors (<!--[[-->, <!--]]-->)
|
||||||
|
isDynamicAnchor(node) ||
|
||||||
|
// fragment end anchor (`<!--]-->`)
|
||||||
|
isComment(node, ']') ||
|
||||||
|
isDynamicFragmentAnchor(node)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDynamicFragmentAnchor(node: Node) {
|
||||||
|
return __DEV__
|
||||||
|
? // v-if anchor (`<!--if-->`)
|
||||||
|
isComment(node, 'if') ||
|
||||||
|
// v-for anchor (`<!--for-->`)
|
||||||
|
isComment(node, 'for') ||
|
||||||
|
// v-slot anchor (`<!--slot-->`)
|
||||||
|
isComment(node, 'slot') ||
|
||||||
|
// dynamic-component anchor (`<!--dynamic-component-->`)
|
||||||
|
isComment(node, 'dynamic-component')
|
||||||
|
: // TODO ?
|
||||||
|
isComment(node, '$')
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue