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

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

View File

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

View File

@@ -451,14 +451,14 @@ describe('Suspense', () => {
await deps[0]
await nextTick()
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`])
await Promise.all(deps)
await nextTick()
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`])
})
@@ -522,7 +522,7 @@ describe('Suspense', () => {
await Promise.all(deps)
await nextTick()
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`])
})
@@ -663,7 +663,7 @@ describe('Suspense', () => {
await deps[3]
await nextTick()
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])
@@ -674,7 +674,7 @@ describe('Suspense', () => {
await Promise.all(deps)
await nextTick()
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])
@@ -682,7 +682,7 @@ describe('Suspense', () => {
msg.value = 'nested changed again'
await nextTick()
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 nextTick()
expect(serializeInner(root)).toBe(`<!----><div>Child A</div><!----><!---->`)
expect(serializeInner(root)).toBe(`<div>Child A</div><!---->`)
toggle.value = true
await nextTick()
@@ -725,9 +725,7 @@ describe('Suspense', () => {
await deps[1]
await nextTick()
expect(serializeInner(root)).toBe(
`<!----><div>Child A</div><div>Child B</div><!---->`
)
expect(serializeInner(root)).toBe(`<div>Child A</div><div>Child B</div>`)
})
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 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 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 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>"`;

View File

@@ -60,13 +60,13 @@ describe('hot module replacement', () => {
createRecord(parentId, Parent)
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
// re-render!
triggerEvent(root.children[0] as TestElement, 'click')
await nextTick()
expect(serializeInner(root)).toBe(`<div>1<!---->1<!----></div>`)
expect(serializeInner(root)).toBe(`<div>11</div>`)
// Update text while preserving state
rerender(
@@ -75,7 +75,7 @@ describe('hot module replacement', () => {
`<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
rerender(
@@ -84,7 +84,7 @@ describe('hot module replacement', () => {
`<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
rerender(
@@ -95,9 +95,7 @@ describe('hot module replacement', () => {
</div>`
)
)
expect(serializeInner(root)).toBe(
`<div>1<span>1</span><!---->1!<!----></div>`
)
expect(serializeInner(root)).toBe(`<div>1<span>1</span>1!</div>`)
// Should force update child slot elements
rerender(
@@ -108,7 +106,7 @@ describe('hot module replacement', () => {
</div>`
)
)
expect(serializeInner(root)).toBe(`<div><!----><span>1</span><!----></div>`)
expect(serializeInner(root)).toBe(`<div><span>1</span></div>`)
})
test('reload', async () => {

View File

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

View File

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

View File

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

View File

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