refactor(fragments): remove visible anchors for fragments

This commit is contained in:
Evan You 2020-02-26 16:32:06 -05:00
parent 439752822c
commit 11d2fb2594
19 changed files with 95 additions and 192 deletions

View File

@ -219,11 +219,11 @@ describe('ssr: components', () => {
foo: ({ list }, _push, _parent, _scopeId) => { foo: ({ list }, _push, _parent, _scopeId) => {
if (_push) { if (_push) {
if (_ctx.ok) { if (_ctx.ok) {
_push(\`<div\${_scopeId}><!---->\`) _push(\`<div\${_scopeId}>\`)
_ssrRenderList(list, (i) => { _ssrRenderList(list, (i) => {
_push(\`<span\${_scopeId}></span>\`) _push(\`<span\${_scopeId}></span>\`)
}) })
_push(\`<!----></div>\`) _push(\`</div>\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -242,11 +242,11 @@ describe('ssr: components', () => {
bar: ({ ok }, _push, _parent, _scopeId) => { bar: ({ ok }, _push, _parent, _scopeId) => {
if (_push) { if (_push) {
if (ok) { if (ok) {
_push(\`<div\${_scopeId}><!---->\`) _push(\`<div\${_scopeId}>\`)
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<span\${_scopeId}></span>\`) _push(\`<span\${_scopeId}></span>\`)
}) })
_push(\`<!----></div>\`) _push(\`</div>\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -283,7 +283,7 @@ describe('ssr: components', () => {
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
" "
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!----><div></div><!---->\`) _push(\`<div></div>\`)
}" }"
`) `)
@ -305,7 +305,7 @@ describe('ssr: components', () => {
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
" "
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!----><div></div><!---->\`) _push(\`<div></div>\`)
}" }"
`) `)
}) })

View File

@ -6,11 +6,9 @@ describe('ssr: v-for', () => {
"const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\") "const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!---->\`)
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}) })
_push(\`<!---->\`)
}" }"
`) `)
}) })
@ -21,11 +19,9 @@ describe('ssr: v-for', () => {
"const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\") "const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!---->\`)
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<div>foo<span>bar</span></div>\`) _push(\`<div>foo<span>bar</span></div>\`)
}) })
_push(\`<!---->\`)
}" }"
`) `)
}) })
@ -41,9 +37,8 @@ describe('ssr: v-for', () => {
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\") "const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!---->\`)
_ssrRenderList(_ctx.list, (row, i) => { _ssrRenderList(_ctx.list, (row, i) => {
_push(\`<div><!---->\`) _push(\`<div>\`)
_ssrRenderList(row, (j) => { _ssrRenderList(row, (j) => {
_push(\`<div>\${ _push(\`<div>\${
_ssrInterpolate(i) _ssrInterpolate(i)
@ -51,9 +46,8 @@ describe('ssr: v-for', () => {
_ssrInterpolate(j) _ssrInterpolate(j)
}</div>\`) }</div>\`)
}) })
_push(\`<!----></div>\`) _push(\`</div>\`)
}) })
_push(\`<!---->\`)
}" }"
`) `)
}) })
@ -64,11 +58,9 @@ describe('ssr: v-for', () => {
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\") "const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!---->\`)
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<!---->\${_ssrInterpolate(i)}<!---->\`) _push(\`\${_ssrInterpolate(i)}\`)
}) })
_push(\`<!---->\`)
}" }"
`) `)
}) })
@ -81,11 +73,9 @@ describe('ssr: v-for', () => {
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\") "const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!---->\`)
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<span>\${_ssrInterpolate(i)}</span>\`) _push(\`<span>\${_ssrInterpolate(i)}</span>\`)
}) })
_push(\`<!---->\`)
}" }"
`) `)
}) })
@ -99,15 +89,13 @@ describe('ssr: v-for', () => {
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\") "const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!---->\`)
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<!----><span>\${ _push(\`<span>\${
_ssrInterpolate(i) _ssrInterpolate(i)
}</span><span>\${ }</span><span>\${
_ssrInterpolate(i + 1) _ssrInterpolate(i + 1)
}</span><!---->\`) }</span>\`)
}) })
_push(\`<!---->\`)
}" }"
`) `)
}) })
@ -123,11 +111,9 @@ describe('ssr: v-for', () => {
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\") "const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
_push(\`<!---->\`)
_ssrRenderList(_ctx.list, ({ foo }, index) => { _ssrRenderList(_ctx.list, ({ foo }, index) => {
_push(\`<div>\${_ssrInterpolate(foo + _ctx.bar + index)}</div>\`) _push(\`<div>\${_ssrInterpolate(foo + _ctx.bar + index)}</div>\`)
}) })
_push(\`<!---->\`)
}" }"
`) `)
}) })

View File

@ -80,7 +80,7 @@ describe('ssr: v-if', () => {
" "
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<!---->hello<!---->\`) _push(\`hello\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -110,7 +110,7 @@ describe('ssr: v-if', () => {
" "
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<!----><div>hi</div><div>ho</div><!---->\`) _push(\`<div>hi</div><div>ho</div>\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -126,11 +126,9 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<!---->\`)
_ssrRenderList(_ctx.list, (i) => { _ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}) })
_push(\`<!---->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -147,7 +145,7 @@ describe('ssr: v-if', () => {
" "
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<!----><div>hi</div><div>ho</div><!---->\`) _push(\`<div>hi</div><div>ho</div>\`)
} else { } else {
_push(\`<div></div>\`) _push(\`<div></div>\`)
} }

View File

@ -9,7 +9,6 @@ import {
ElementTypes, ElementTypes,
createBlockStatement, createBlockStatement,
CompilerOptions, CompilerOptions,
isText,
IfStatement, IfStatement,
CallExpression CallExpression
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
@ -29,9 +28,7 @@ import { ssrProcessElement } from './transforms/ssrTransformElement'
export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) { export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
const context = createSSRTransformContext(ast, options) const context = createSSRTransformContext(ast, options)
const isFragment = processChildren(ast.children, context)
ast.children.length > 1 && ast.children.some(c => !isText(c))
processChildren(ast.children, context, isFragment)
ast.codegenNode = createBlockStatement(context.body) ast.codegenNode = createBlockStatement(context.body)
// Finalize helpers. // Finalize helpers.
@ -107,12 +104,8 @@ function createChildContext(
export function processChildren( export function processChildren(
children: TemplateChildNode[], children: TemplateChildNode[],
context: SSRTransformContext, context: SSRTransformContext
asFragment = false
) { ) {
if (asFragment) {
context.pushStringPart(`<!---->`)
}
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = children[i] const child = children[i]
if (child.type === NodeTypes.ELEMENT) { if (child.type === NodeTypes.ELEMENT) {
@ -135,18 +128,14 @@ export function processChildren(
ssrProcessFor(child, context) ssrProcessFor(child, context)
} }
} }
if (asFragment) {
context.pushStringPart(`<!---->`)
}
} }
export function processChildrenAsStatement( export function processChildrenAsStatement(
children: TemplateChildNode[], children: TemplateChildNode[],
parentContext: SSRTransformContext, parentContext: SSRTransformContext,
asFragment = false,
withSlotScopeId = parentContext.withSlotScopeId withSlotScopeId = parentContext.withSlotScopeId
): BlockStatement { ): BlockStatement {
const childContext = createChildContext(parentContext, withSlotScopeId) const childContext = createChildContext(parentContext, withSlotScopeId)
processChildren(children, childContext, asFragment) processChildren(children, childContext)
return createBlockStatement(childContext.body) return createBlockStatement(childContext.body)
} }

View File

@ -12,8 +12,6 @@ import {
FunctionExpression, FunctionExpression,
TemplateChildNode, TemplateChildNode,
PORTAL, PORTAL,
SUSPENSE,
TRANSITION_GROUP,
createIfStatement, createIfStatement,
createSimpleExpression, createSimpleExpression,
getBaseTransformPreset, getBaseTransformPreset,
@ -135,14 +133,10 @@ export function ssrProcessComponent(
// this is a built-in component that fell-through. // this is a built-in component that fell-through.
// just render its children. // just render its children.
const component = componentTypeMap.get(node)! const component = componentTypeMap.get(node)!
if (component === PORTAL) { if (component === PORTAL) {
return ssrProcessPortal(node, context) return ssrProcessPortal(node, context)
} }
processChildren(node.children, context)
const needFragmentWrapper =
component === SUSPENSE || component === TRANSITION_GROUP
processChildren(node.children, context, needFragmentWrapper)
} else { } else {
// finish up slot function expressions from the 1st pass. // finish up slot function expressions from the 1st pass.
const wipEntries = wipMap.get(node) || [] const wipEntries = wipMap.get(node) || []
@ -157,7 +151,6 @@ export function ssrProcessComponent(
processChildrenAsStatement( processChildrenAsStatement(
children, children,
context, context,
false,
true /* withSlotScopeId */ true /* withSlotScopeId */
), ),
vnodeBranch vnodeBranch

View File

@ -4,8 +4,7 @@ import {
processFor, processFor,
createCallExpression, createCallExpression,
createFunctionExpression, createFunctionExpression,
createForLoopParams, createForLoopParams
NodeTypes
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { import {
SSRTransformContext, SSRTransformContext,
@ -22,24 +21,14 @@ export const ssrTransformFor = createStructuralDirectiveTransform(
// This is called during the 2nd transform pass to construct the SSR-sepcific // This is called during the 2nd transform pass to construct the SSR-sepcific
// codegen nodes. // codegen nodes.
export function ssrProcessFor(node: ForNode, context: SSRTransformContext) { export function ssrProcessFor(node: ForNode, context: SSRTransformContext) {
const needFragmentWrapper =
node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT
const renderLoop = createFunctionExpression( const renderLoop = createFunctionExpression(
createForLoopParams(node.parseResult) createForLoopParams(node.parseResult)
) )
renderLoop.body = processChildrenAsStatement( renderLoop.body = processChildrenAsStatement(node.children, context)
node.children,
context,
needFragmentWrapper
)
// v-for always renders a fragment
context.pushStringPart(`<!---->`)
context.pushStatement( context.pushStatement(
createCallExpression(context.helper(SSR_RENDER_LIST), [ createCallExpression(context.helper(SSR_RENDER_LIST), [
node.source, node.source,
renderLoop renderLoop
]) ])
) )
context.pushStringPart(`<!---->`)
} }

View File

@ -4,10 +4,7 @@ import {
IfNode, IfNode,
createIfStatement, createIfStatement,
createBlockStatement, createBlockStatement,
createCallExpression, createCallExpression
IfBranchNode,
BlockStatement,
NodeTypes
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { import {
SSRTransformContext, SSRTransformContext,
@ -26,14 +23,17 @@ export function ssrProcessIf(node: IfNode, context: SSRTransformContext) {
const [rootBranch] = node.branches const [rootBranch] = node.branches
const ifStatement = createIfStatement( const ifStatement = createIfStatement(
rootBranch.condition!, rootBranch.condition!,
processIfBranch(rootBranch, context) processChildrenAsStatement(rootBranch.children, context)
) )
context.pushStatement(ifStatement) context.pushStatement(ifStatement)
let currentIf = ifStatement let currentIf = ifStatement
for (let i = 1; i < node.branches.length; i++) { for (let i = 1; i < node.branches.length; i++) {
const branch = node.branches[i] const branch = node.branches[i]
const branchBlockStatement = processIfBranch(branch, context) const branchBlockStatement = processChildrenAsStatement(
branch.children,
context
)
if (branch.condition) { if (branch.condition) {
// else-if // else-if
currentIf = currentIf.alternate = createIfStatement( currentIf = currentIf.alternate = createIfStatement(
@ -52,15 +52,3 @@ export function ssrProcessIf(node: IfNode, context: SSRTransformContext) {
]) ])
} }
} }
function processIfBranch(
branch: IfBranchNode,
context: SSRTransformContext
): BlockStatement {
const { children } = branch
const needFragmentWrapper =
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
// optimize away nested fragments when the only child is a ForNode
!(children.length === 1 && children[0].type === NodeTypes.FOR)
return processChildrenAsStatement(children, context, needFragmentWrapper)
}

View File

@ -281,7 +281,7 @@ describe('api: options', () => {
} }
} as any } as any
expect(renderToString(h(Root))).toBe(`<!---->1112<!---->`) expect(renderToString(h(Root))).toBe(`1112`)
}) })
test('lifecycle', async () => { test('lifecycle', async () => {

View File

@ -6,7 +6,6 @@ import {
defineComponent, defineComponent,
Portal, Portal,
Text, Text,
Fragment,
ref, ref,
nextTick, nextTick,
TestElement, TestElement,
@ -19,12 +18,10 @@ describe('renderer: portal', () => {
const target = nodeOps.createElement('div') const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div') const root = nodeOps.createElement('div')
const Comp = defineComponent(() => () => const Comp = defineComponent(() => () => [
h(Fragment, [
h(Portal, { target }, h('div', 'teleported')), h(Portal, { target }, h('div', 'teleported')),
h('div', 'root') h('div', 'root')
]) ])
)
render(h(Comp), root) render(h(Comp), root)
expect(serializeInner(root)).toMatchSnapshot() expect(serializeInner(root)).toMatchSnapshot()
@ -37,12 +34,10 @@ describe('renderer: portal', () => {
const target = ref(targetA) const target = ref(targetA)
const root = nodeOps.createElement('div') const root = nodeOps.createElement('div')
const Comp = defineComponent(() => () => const Comp = defineComponent(() => () => [
h(Fragment, [
h(Portal, { target: target.value }, h('div', 'teleported')), h(Portal, { target: target.value }, h('div', 'teleported')),
h('div', 'root') h('div', 'root')
]) ])
)
render(h(Comp), root) render(h(Comp), root)
expect(serializeInner(root)).toMatchSnapshot() expect(serializeInner(root)).toMatchSnapshot()

View File

@ -451,14 +451,14 @@ describe('Suspense', () => {
await deps[0] await deps[0]
await nextTick() await nextTick()
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(
`<!----><div>async outer</div><div>fallback inner</div><!---->` `<div>async outer</div><div>fallback inner</div>`
) )
expect(calls).toEqual([`outer mounted`]) expect(calls).toEqual([`outer mounted`])
await Promise.all(deps) await Promise.all(deps)
await nextTick() await nextTick()
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(
`<!----><div>async outer</div><div>async inner</div><!---->` `<div>async outer</div><div>async inner</div>`
) )
expect(calls).toEqual([`outer mounted`, `inner mounted`]) expect(calls).toEqual([`outer mounted`, `inner mounted`])
}) })
@ -522,7 +522,7 @@ describe('Suspense', () => {
await Promise.all(deps) await Promise.all(deps)
await nextTick() await nextTick()
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(
`<!----><div>async outer</div><div>async inner</div><!---->` `<div>async outer</div><div>async inner</div>`
) )
expect(calls).toEqual([`inner mounted`, `outer mounted`]) expect(calls).toEqual([`inner mounted`, `outer mounted`])
}) })
@ -663,7 +663,7 @@ describe('Suspense', () => {
await deps[3] await deps[3]
await nextTick() await nextTick()
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(
`<!----><div>nested fallback</div><div>root async</div><!---->` `<div>nested fallback</div><div>root async</div>`
) )
expect(calls).toEqual([0, 1, 3]) expect(calls).toEqual([0, 1, 3])
@ -674,7 +674,7 @@ describe('Suspense', () => {
await Promise.all(deps) await Promise.all(deps)
await nextTick() await nextTick()
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(
`<!----><div>nested changed</div><div>root async</div><!---->` `<div>nested changed</div><div>root async</div>`
) )
expect(calls).toEqual([0, 1, 3, 2]) expect(calls).toEqual([0, 1, 3, 2])
@ -682,7 +682,7 @@ describe('Suspense', () => {
msg.value = 'nested changed again' msg.value = 'nested changed again'
await nextTick() await nextTick()
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(
`<!----><div>nested changed again</div><div>root async</div><!---->` `<div>nested changed again</div><div>root async</div>`
) )
}) })
@ -717,7 +717,7 @@ describe('Suspense', () => {
await deps[0] await deps[0]
await nextTick() await nextTick()
expect(serializeInner(root)).toBe(`<!----><div>Child A</div><!----><!---->`) expect(serializeInner(root)).toBe(`<div>Child A</div><!---->`)
toggle.value = true toggle.value = true
await nextTick() await nextTick()
@ -725,9 +725,7 @@ describe('Suspense', () => {
await deps[1] await deps[1]
await nextTick() await nextTick()
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(`<div>Child A</div><div>Child B</div>`)
`<!----><div>Child A</div><div>Child B</div><!---->`
)
}) })
test.todo('portal inside suspense') test.todo('portal inside suspense')

View File

@ -6,18 +6,18 @@ exports[`renderer: portal should update children 2`] = `""`;
exports[`renderer: portal should update children 3`] = `"teleported"`; exports[`renderer: portal should update children 3`] = `"teleported"`;
exports[`renderer: portal should update target 1`] = `"<!----><!--[object Object]--><div>root</div><!---->"`; exports[`renderer: portal should update target 1`] = `"<!--portal--><div>root</div>"`;
exports[`renderer: portal should update target 2`] = `"<div>teleported</div>"`; exports[`renderer: portal should update target 2`] = `"<div>teleported</div>"`;
exports[`renderer: portal should update target 3`] = `""`; exports[`renderer: portal should update target 3`] = `""`;
exports[`renderer: portal should update target 4`] = `"<!----><!--[object Object]--><div>root</div><!---->"`; exports[`renderer: portal should update target 4`] = `"<!--portal--><div>root</div>"`;
exports[`renderer: portal should update target 5`] = `""`; exports[`renderer: portal should update target 5`] = `""`;
exports[`renderer: portal should update target 6`] = `"<div>teleported</div>"`; exports[`renderer: portal should update target 6`] = `"<div>teleported</div>"`;
exports[`renderer: portal should work 1`] = `"<!----><!--[object Object]--><div>root</div><!---->"`; exports[`renderer: portal should work 1`] = `"<!--portal--><div>root</div>"`;
exports[`renderer: portal should work 2`] = `"<div>teleported</div>"`; exports[`renderer: portal should work 2`] = `"<div>teleported</div>"`;

View File

@ -60,13 +60,13 @@ describe('hot module replacement', () => {
createRecord(parentId, Parent) createRecord(parentId, Parent)
render(h(Parent), root) render(h(Parent), root)
expect(serializeInner(root)).toBe(`<div>0<!---->0<!----></div>`) expect(serializeInner(root)).toBe(`<div>00</div>`)
// Perform some state change. This change should be preserved after the // Perform some state change. This change should be preserved after the
// re-render! // re-render!
triggerEvent(root.children[0] as TestElement, 'click') triggerEvent(root.children[0] as TestElement, 'click')
await nextTick() await nextTick()
expect(serializeInner(root)).toBe(`<div>1<!---->1<!----></div>`) expect(serializeInner(root)).toBe(`<div>11</div>`)
// Update text while preserving state // Update text while preserving state
rerender( rerender(
@ -75,7 +75,7 @@ describe('hot module replacement', () => {
`<div @click="count++">{{ count }}!<Child>{{ count }}</Child></div>` `<div @click="count++">{{ count }}!<Child>{{ count }}</Child></div>`
) )
) )
expect(serializeInner(root)).toBe(`<div>1!<!---->1<!----></div>`) expect(serializeInner(root)).toBe(`<div>1!1</div>`)
// Should force child update on slot content change // Should force child update on slot content change
rerender( rerender(
@ -84,7 +84,7 @@ describe('hot module replacement', () => {
`<div @click="count++">{{ count }}!<Child>{{ count }}!</Child></div>` `<div @click="count++">{{ count }}!<Child>{{ count }}!</Child></div>`
) )
) )
expect(serializeInner(root)).toBe(`<div>1!<!---->1!<!----></div>`) expect(serializeInner(root)).toBe(`<div>1!1!</div>`)
// Should force update element children despite block optimization // Should force update element children despite block optimization
rerender( rerender(
@ -95,9 +95,7 @@ describe('hot module replacement', () => {
</div>` </div>`
) )
) )
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(`<div>1<span>1</span>1!</div>`)
`<div>1<span>1</span><!---->1!<!----></div>`
)
// Should force update child slot elements // Should force update child slot elements
rerender( rerender(
@ -108,7 +106,7 @@ describe('hot module replacement', () => {
</div>` </div>`
) )
) )
expect(serializeInner(root)).toBe(`<div><!----><span>1</span><!----></div>`) expect(serializeInner(root)).toBe(`<div><span>1</span></div>`)
}) })
test('reload', async () => { test('reload', async () => {

View File

@ -322,9 +322,7 @@ describe('attribute fallthrough', () => {
render(h(Parent), root) render(h(Parent), root)
expect(`Extraneous non-props attributes`).not.toHaveBeenWarned() expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
expect(root.innerHTML).toBe( expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
`<!----><div></div><div class="parent"></div><!---->`
)
}) })
it('should not warn when context.attrs is used during render', () => { it('should not warn when context.attrs is used during render', () => {
@ -346,8 +344,6 @@ describe('attribute fallthrough', () => {
render(h(Parent), root) render(h(Parent), root)
expect(`Extraneous non-props attributes`).not.toHaveBeenWarned() expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
expect(root.innerHTML).toBe( expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
`<!----><div></div><div class="parent"></div><!---->`
)
}) })
}) })

View File

@ -25,10 +25,11 @@ describe('renderer: fragment', () => {
const root = nodeOps.createElement('div') const root = nodeOps.createElement('div')
render(h(App), root) render(h(App), root)
expect(serializeInner(root)).toBe(`<!----><div>one</div>two<!---->`) expect(serializeInner(root)).toBe(`<div>one</div>two`)
expect(root.children.length).toBe(4) expect(root.children.length).toBe(4)
expect(root.children[0]).toMatchObject({ expect(root.children[0]).toMatchObject({
type: NodeTypes.COMMENT type: NodeTypes.TEXT,
text: ''
}) })
expect(root.children[1]).toMatchObject({ expect(root.children[1]).toMatchObject({
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
@ -43,7 +44,8 @@ describe('renderer: fragment', () => {
text: 'two' text: 'two'
}) })
expect(root.children[3]).toMatchObject({ expect(root.children[3]).toMatchObject({
type: NodeTypes.COMMENT type: NodeTypes.TEXT,
text: ''
}) })
}) })
@ -51,7 +53,7 @@ describe('renderer: fragment', () => {
const root = nodeOps.createElement('div') const root = nodeOps.createElement('div')
render(h('div', [h(Fragment, [h('div', 'one'), 'two'])]), root) render(h('div', [h(Fragment, [h('div', 'one'), 'two'])]), root)
const parent = root.children[0] as TestElement const parent = root.children[0] as TestElement
expect(serializeInner(parent)).toBe(`<!----><div>one</div>two<!---->`) expect(serializeInner(parent)).toBe(`<div>one</div>two`)
}) })
it('patch fragment children (manual, keyed)', () => { it('patch fragment children (manual, keyed)', () => {
@ -60,18 +62,14 @@ describe('renderer: fragment', () => {
h(Fragment, [h('div', { key: 1 }, 'one'), h('div', { key: 2 }, 'two')]), h(Fragment, [h('div', { key: 1 }, 'one'), h('div', { key: 2 }, 'two')]),
root root
) )
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
`<!----><div>one</div><div>two</div><!---->`
)
resetOps() resetOps()
render( render(
h(Fragment, [h('div', { key: 2 }, 'two'), h('div', { key: 1 }, 'one')]), h(Fragment, [h('div', { key: 2 }, 'two'), h('div', { key: 1 }, 'one')]),
root root
) )
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
`<!----><div>two</div><div>one</div><!---->`
)
const ops = dumpOps() const ops = dumpOps()
// should be moving nodes instead of re-creating or patching them // should be moving nodes instead of re-creating or patching them
expect(ops).toMatchObject([ expect(ops).toMatchObject([
@ -84,15 +82,11 @@ describe('renderer: fragment', () => {
it('patch fragment children (manual, unkeyed)', () => { it('patch fragment children (manual, unkeyed)', () => {
const root = nodeOps.createElement('div') const root = nodeOps.createElement('div')
render(h(Fragment, [h('div', 'one'), h('div', 'two')]), root) render(h(Fragment, [h('div', 'one'), h('div', 'two')]), root)
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
`<!----><div>one</div><div>two</div><!---->`
)
resetOps() resetOps()
render(h(Fragment, [h('div', 'two'), h('div', 'one')]), root) render(h(Fragment, [h('div', 'two'), h('div', 'one')]), root)
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
`<!----><div>two</div><div>one</div><!---->`
)
const ops = dumpOps() const ops = dumpOps()
// should be patching nodes instead of moving or re-creating them // should be patching nodes instead of moving or re-creating them
expect(ops).toMatchObject([ expect(ops).toMatchObject([
@ -119,7 +113,7 @@ describe('renderer: fragment', () => {
), ),
root root
) )
expect(serializeInner(root)).toBe(`<!----><div>one</div>two<!---->`) expect(serializeInner(root)).toBe(`<div>one</div>two`)
render( render(
createVNode( createVNode(
@ -134,7 +128,7 @@ describe('renderer: fragment', () => {
), ),
root root
) )
expect(serializeInner(root)).toBe(`<!----><div>foo</div>barbaz<!---->`) expect(serializeInner(root)).toBe(`<div>foo</div>barbaz`)
}) })
it('patch fragment children (compiler generated, keyed)', () => { it('patch fragment children (compiler generated, keyed)', () => {
@ -149,9 +143,7 @@ describe('renderer: fragment', () => {
), ),
root root
) )
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
`<!----><div>one</div><div>two</div><!---->`
)
resetOps() resetOps()
render( render(
@ -163,9 +155,7 @@ describe('renderer: fragment', () => {
), ),
root root
) )
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
`<!----><div>two</div><div>one</div><!---->`
)
const ops = dumpOps() const ops = dumpOps()
// should be moving nodes instead of re-creating or patching them // should be moving nodes instead of re-creating or patching them
expect(ops).toMatchObject([ expect(ops).toMatchObject([
@ -188,7 +178,7 @@ describe('renderer: fragment', () => {
root root
) )
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(
`<div><div>outer</div><!----><div>one</div><div>two</div><!----></div>` `<div><div>outer</div><div>one</div><div>two</div></div>`
) )
resetOps() resetOps()
@ -203,7 +193,7 @@ describe('renderer: fragment', () => {
root root
) )
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(
`<div><!----><div>two</div><div>one</div><!----><div>outer</div></div>` `<div><div>two</div><div>one</div><div>outer</div></div>`
) )
const ops = dumpOps() const ops = dumpOps()
// should be moving nodes instead of re-creating them // should be moving nodes instead of re-creating them
@ -213,10 +203,10 @@ describe('renderer: fragment', () => {
// 2. move entire fragment, including anchors // 2. move entire fragment, including anchors
// not the most efficient move, but this case is super rare // not the most efficient move, but this case is super rare
// and optimizing for this special case complicates the algo quite a bit // and optimizing for this special case complicates the algo quite a bit
{ type: NodeOpTypes.INSERT, targetNode: { type: 'comment' } }, { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } },
{ type: NodeOpTypes.INSERT, targetNode: { type: 'element' } }, { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
{ type: NodeOpTypes.INSERT, targetNode: { type: 'element' } }, { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
{ type: NodeOpTypes.INSERT, targetNode: { type: 'comment' } } { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } }
]) ])
}) })
@ -234,7 +224,7 @@ describe('renderer: fragment', () => {
root root
) )
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(
`<!----><div>outer</div><!----><div>one</div><div>two</div><!----><!---->` `<div>outer</div><div>one</div><div>two</div>`
) )
resetOps() resetOps()
@ -249,16 +239,16 @@ describe('renderer: fragment', () => {
root root
) )
expect(serializeInner(root)).toBe( expect(serializeInner(root)).toBe(
`<!----><!----><div>two</div><div>one</div><!----><div>outer</div><!---->` `<div>two</div><div>one</div><div>outer</div>`
) )
const ops = dumpOps() const ops = dumpOps()
// should be moving nodes instead of re-creating them // should be moving nodes instead of re-creating them
expect(ops).toMatchObject([ expect(ops).toMatchObject([
{ type: NodeOpTypes.INSERT, targetNode: { type: 'element' } }, { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
{ type: NodeOpTypes.INSERT, targetNode: { type: 'comment' } }, { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } },
{ type: NodeOpTypes.INSERT, targetNode: { type: 'element' } }, { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
{ type: NodeOpTypes.INSERT, targetNode: { type: 'element' } }, { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
{ type: NodeOpTypes.INSERT, targetNode: { type: 'comment' } } { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } }
]) ])
// should properly remove nested fragments // should properly remove nested fragments

View File

@ -24,7 +24,7 @@ export type RootHydrateFunction = (
// passed in via arguments. // passed in via arguments.
export function createHydrationFunctions({ export function createHydrationFunctions({
mt: mountComponent, mt: mountComponent,
o: { patchProp } o: { patchProp, createText }
}: RendererInternals<Node, Element>) { }: RendererInternals<Node, Element>) {
const hydrate: RootHydrateFunction = (vnode, container) => { const hydrate: RootHydrateFunction = (vnode, container) => {
if (__DEV__ && !container.hasChildNodes()) { if (__DEV__ && !container.hasChildNodes()) {
@ -40,7 +40,7 @@ export function createHydrationFunctions({
node: Node, node: Node,
vnode: VNode, vnode: VNode,
parentComponent: ComponentInternalInstance | null = null parentComponent: ComponentInternalInstance | null = null
): Node | null | undefined => { ): Node | null => {
const { type, shapeFlag } = vnode const { type, shapeFlag } = vnode
vnode.el = node vnode.el = node
switch (type) { switch (type) {
@ -49,14 +49,15 @@ export function createHydrationFunctions({
case Static: case Static:
return node.nextSibling return node.nextSibling
case Fragment: case Fragment:
const anchor = (vnode.anchor = hydrateChildren( const parent = node.parentNode!
node.nextSibling, parent.insertBefore((vnode.el = createText('')), node)
const next = hydrateChildren(
node,
vnode.children as VNode[], vnode.children as VNode[],
parentComponent parentComponent
)!) )
// TODO handle potential hydration error if fragment didn't get parent.insertBefore((vnode.anchor = createText('')), next)
// back anchor as expected. return next
return anchor.nextSibling
default: default:
if (shapeFlag & ShapeFlags.ELEMENT) { if (shapeFlag & ShapeFlags.ELEMENT) {
return hydrateElement(node as Element, vnode, parentComponent) return hydrateElement(node as Element, vnode, parentComponent)
@ -75,6 +76,7 @@ export function createHydrationFunctions({
} else if (__DEV__) { } else if (__DEV__) {
warn('Invalid HostVNode type:', type, `(${typeof type})`) warn('Invalid HostVNode type:', type, `(${typeof type})`)
} }
return null
} }
} }
@ -130,10 +132,10 @@ export function createHydrationFunctions({
} }
const hydrateChildren = ( const hydrateChildren = (
node: Node | null | undefined, node: Node | null,
vnodes: VNode[], vnodes: VNode[],
parentComponent: ComponentInternalInstance | null parentComponent: ComponentInternalInstance | null
): Node | null | undefined => { ): Node | null => {
for (let i = 0; node != null && i < vnodes.length; i++) { for (let i = 0; node != null && i < vnodes.length; i++) {
// TODO can skip normalizeVNode in optimized mode // TODO can skip normalizeVNode in optimized mode
// (need hint on rendered markup?) // (need hint on rendered markup?)

View File

@ -126,7 +126,6 @@ export interface RendererInternals<HostNode = any, HostElement = any> {
pbc: PatchBlockChildrenFn<HostNode, HostElement> pbc: PatchBlockChildrenFn<HostNode, HostElement>
n: NextFn<HostNode, HostElement> n: NextFn<HostNode, HostElement>
o: RendererOptions<HostNode, HostElement> o: RendererOptions<HostNode, HostElement>
c: ProcessTextOrCommentFn<HostNode, HostElement>
} }
// These functions are created inside a closure and therefore their types cannot // These functions are created inside a closure and therefore their types cannot
@ -845,8 +844,6 @@ function baseCreateRenderer<
} }
} }
let devFragmentID = 0
const processFragment = ( const processFragment = (
n1: HostVNode | null, n1: HostVNode | null,
n2: HostVNode, n2: HostVNode,
@ -857,13 +854,8 @@ function baseCreateRenderer<
isSVG: boolean, isSVG: boolean,
optimized: boolean optimized: boolean
) => { ) => {
const showID = __DEV__ && !__TEST__ const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
const fragmentStartAnchor = (n2.el = n1 const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!
? n1.el
: hostCreateComment(showID ? `fragment-${devFragmentID}-start` : ''))!
const fragmentEndAnchor = (n2.anchor = n1
? n1.anchor
: hostCreateComment(showID ? `fragment-${devFragmentID}-end` : ''))!
let { patchFlag, dynamicChildren } = n2 let { patchFlag, dynamicChildren } = n2
if (patchFlag > 0) { if (patchFlag > 0) {
@ -878,9 +870,6 @@ function baseCreateRenderer<
} }
if (n1 == null) { if (n1 == null) {
if (showID) {
devFragmentID++
}
hostInsert(fragmentStartAnchor, container, anchor) hostInsert(fragmentStartAnchor, container, anchor)
hostInsert(fragmentEndAnchor, container, anchor) hostInsert(fragmentEndAnchor, container, anchor)
// a fragment can only have array children // a fragment can only have array children
@ -1864,7 +1853,6 @@ function baseCreateRenderer<
pc: patchChildren, pc: patchChildren,
pbc: patchBlockChildren, pbc: patchBlockChildren,
n: getNextHostNode, n: getNextHostNode,
c: processCommentNode,
o: options o: options
} }

View File

@ -262,7 +262,7 @@ describe('ssr: renderToString', () => {
) )
).toBe( ).toBe(
`<div>parent<div class="child">` + `<div>parent<div class="child">` +
`<!----><span>from slot</span><!---->` + `<span>from slot</span>` +
`</div></div>` `</div></div>`
) )
@ -277,7 +277,7 @@ describe('ssr: renderToString', () => {
} }
}) })
) )
).toBe(`<div>parent<div class="child"><!---->fallback<!----></div></div>`) ).toBe(`<div>parent<div class="child">fallback</div></div>`)
}) })
test('nested components with vnode slots', async () => { test('nested components with vnode slots', async () => {
@ -321,7 +321,7 @@ describe('ssr: renderToString', () => {
) )
).toBe( ).toBe(
`<div>parent<div class="child">` + `<div>parent<div class="child">` +
`<!----><span>from slot</span><!---->` + `<span>from slot</span>` +
`</div></div>` `</div></div>`
) )
}) })
@ -339,7 +339,7 @@ describe('ssr: renderToString', () => {
expect(await renderToString(app)).toBe( expect(await renderToString(app)).toBe(
`<div>parent<div class="child">` + `<div>parent<div class="child">` +
`<!----><span>from slot</span><!---->` + `<span>from slot</span>` +
`</div></div>` `</div></div>`
) )
}) })
@ -461,9 +461,7 @@ describe('ssr: renderToString', () => {
createCommentVNode('qux') createCommentVNode('qux')
]) ])
) )
).toBe( ).toBe(`<div>foo<span>bar</span><span>baz</span><!--qux--></div>`)
`<div>foo<span>bar</span><!----><span>baz</span><!----><!--qux--></div>`
)
}) })
test('void elements', async () => { test('void elements', async () => {

View File

@ -19,8 +19,6 @@ export function ssrRenderSlot(
parentComponent: ComponentInternalInstance parentComponent: ComponentInternalInstance
) { ) {
const slotFn = slots[slotName] const slotFn = slots[slotName]
// template-compiled slots are always rendered as fragments
push(`<!---->`)
if (slotFn) { if (slotFn) {
if (slotFn.length > 1) { if (slotFn.length > 1) {
// only ssr-optimized slot fns accept more than 1 arguments // only ssr-optimized slot fns accept more than 1 arguments
@ -33,5 +31,4 @@ export function ssrRenderSlot(
} else if (fallbackRenderFn) { } else if (fallbackRenderFn) {
fallbackRenderFn() fallbackRenderFn()
} }
push(`<!---->`)
} }

View File

@ -238,9 +238,7 @@ function renderVNode(
push(children ? `<!--${children}-->` : `<!---->`) push(children ? `<!--${children}-->` : `<!---->`)
break break
case Fragment: case Fragment:
push(`<!---->`)
renderVNodeChildren(push, children as VNodeArrayChildren, parentComponent) renderVNodeChildren(push, children as VNodeArrayChildren, parentComponent)
push(`<!---->`)
break break
default: default:
if (shapeFlag & ShapeFlags.ELEMENT) { if (shapeFlag & ShapeFlags.ELEMENT) {