feat: fragment

This commit is contained in:
三咲智子 Kevin Deng 2023-11-26 02:13:59 +08:00
parent 9b2a6ffe70
commit ac686033aa
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
12 changed files with 146 additions and 89 deletions

View File

@ -34,6 +34,7 @@ See the To-do list below or `// TODO` comments in code (`compiler-vapor` and `ru
- [ ] compiler - [ ] compiler
- [ ] Remove DOM API in codegen - [ ] Remove DOM API in codegen
- [ ] Fragment - [ ] Fragment
- [x] multiple root nodes
- [ ] Built-in Components - [ ] Built-in Components
- [ ] Transition - [ ] Transition
- [ ] TransitionGroup - [ ] TransitionGroup

View File

@ -5,86 +5,106 @@ exports[`comile > bindings 1`] = `
import { template, children, insert, setText } from 'vue/vapor'; import { template, children, insert, setText } from 'vue/vapor';
const t0 = template(\`<div>count is <!>.</div>\`); const t0 = template(\`<div>count is <!>.</div>\`);
export function render() { export function render() {
const n0 = t0(); const root = t0();
const { const {
1: [n2], 0: [
} = children(n0); n1,
const n1 = document.createTextNode(count.value); {
insert(n1, n0, n2); 1: [n3],
},
],
} = children(root);
const n2 = document.createTextNode(count.value);
insert(n2, n1, n3);
watchEffect(() => { watchEffect(() => {
setText(n1, undefined, count.value); setText(n2, undefined, count.value);
}); });
return n0; return root;
} }
" "
`; `;
exports[`comile > directives > v-bind > simple expression 1`] = ` exports[`comile > directives > v-bind > simple expression 1`] = `
"import { watchEffect } from 'vue'; "import { watchEffect } from 'vue';
import { template, setAttr } from 'vue/vapor'; import { template, children, setAttr } from 'vue/vapor';
const t0 = template(\`<div></div>\`); const t0 = template(\`<div></div>\`);
export function render() { export function render() {
const n0 = t0(); const root = t0();
const {
0: [n1],
} = children(root);
watchEffect(() => { watchEffect(() => {
setAttr(n0, 'id', undefined, id.value); setAttr(n1, 'id', undefined, id.value);
}); });
return n0; return root;
} }
" "
`; `;
exports[`comile > directives > v-html > no expression 1`] = ` exports[`comile > directives > v-html > no expression 1`] = `
"import { watchEffect } from 'vue'; "import { watchEffect } from 'vue';
import { template, setHtml } from 'vue/vapor'; import { template, children, setHtml } from 'vue/vapor';
const t0 = template(\`<div></div>\`); const t0 = template(\`<div></div>\`);
export function render() { export function render() {
const n0 = t0(); const root = t0();
const {
0: [n1],
} = children(root);
watchEffect(() => { watchEffect(() => {
setHtml(n0, undefined, ''); setHtml(n1, undefined, '');
}); });
return n0; return root;
} }
" "
`; `;
exports[`comile > directives > v-html > simple expression 1`] = ` exports[`comile > directives > v-html > simple expression 1`] = `
"import { watchEffect } from 'vue'; "import { watchEffect } from 'vue';
import { template, setHtml } from 'vue/vapor'; import { template, children, setHtml } from 'vue/vapor';
const t0 = template(\`<div></div>\`); const t0 = template(\`<div></div>\`);
export function render() { export function render() {
const n0 = t0(); const root = t0();
const {
0: [n1],
} = children(root);
watchEffect(() => { watchEffect(() => {
setHtml(n0, undefined, code.value); setHtml(n1, undefined, code.value);
}); });
return n0; return root;
} }
" "
`; `;
exports[`comile > directives > v-on > simple expression 1`] = ` exports[`comile > directives > v-on > simple expression 1`] = `
"import { watchEffect } from 'vue'; "import { watchEffect } from 'vue';
import { template, on } from 'vue/vapor'; import { template, children, on } from 'vue/vapor';
const t0 = template(\`<div></div>\`); const t0 = template(\`<div></div>\`);
export function render() { export function render() {
const n0 = t0(); const root = t0();
const {
0: [n1],
} = children(root);
watchEffect(() => { watchEffect(() => {
on(n0, 'click', handleClick); on(n1, 'click', handleClick);
}); });
return n0; return root;
} }
" "
`; `;
exports[`comile > directives > v-once > as root node 1`] = ` exports[`comile > directives > v-once > as root node 1`] = `
"import { watchEffect } from 'vue'; "import { watchEffect } from 'vue';
import { template, setAttr } from 'vue/vapor'; import { template, children, setAttr } from 'vue/vapor';
const t0 = template(\`<div></div>\`); const t0 = template(\`<div></div>\`);
export function render() { export function render() {
const n0 = t0(); const root = t0();
const {
0: [n1],
} = children(root);
watchEffect(() => { watchEffect(() => {
setAttr(n0, 'id', undefined, foo); setAttr(n1, 'id', undefined, foo);
}); });
return n0; return root;
} }
" "
`; `;
@ -93,53 +113,82 @@ exports[`comile > directives > v-once > basic 1`] = `
"import { template, children, insert, setText, setAttr } from 'vue/vapor'; "import { template, children, insert, setText, setAttr } from 'vue/vapor';
const t0 = template(\`<div> <span></span></div>\`); const t0 = template(\`<div> <span></span></div>\`);
export function render() { export function render() {
const n0 = t0(); const root = t0();
const { const {
1: [n2], 0: [
} = children(n0); n1,
const n1 = document.createTextNode(msg.value); {
insert(n1, n0, 0 /* InsertPosition.FIRST */); 1: [n3],
setText(n1, undefined, msg.value); },
setAttr(n2, 'class', undefined, clz.value); ],
return n0; } = children(root);
const n2 = document.createTextNode(msg.value);
insert(n2, n1, 0 /* InsertPosition.FIRST */);
setText(n2, undefined, msg.value);
setAttr(n3, 'class', undefined, clz.value);
return root;
} }
" "
`; `;
exports[`comile > directives > v-text > no expression 1`] = ` exports[`comile > directives > v-text > no expression 1`] = `
"import { watchEffect } from 'vue'; "import { watchEffect } from 'vue';
import { template, setText } from 'vue/vapor'; import { template, children, setText } from 'vue/vapor';
const t0 = template(\`<div></div>\`); const t0 = template(\`<div></div>\`);
export function render() { export function render() {
const n0 = t0(); const root = t0();
const {
0: [n1],
} = children(root);
watchEffect(() => { watchEffect(() => {
setText(n0, undefined, ''); setText(n1, undefined, '');
}); });
return n0; return root;
} }
" "
`; `;
exports[`comile > directives > v-text > simple expression 1`] = ` exports[`comile > directives > v-text > simple expression 1`] = `
"import { watchEffect } from 'vue'; "import { watchEffect } from 'vue';
import { template, setText } from 'vue/vapor'; import { template, children, setText } from 'vue/vapor';
const t0 = template(\`<div></div>\`); const t0 = template(\`<div></div>\`);
export function render() { export function render() {
const n0 = t0(); const root = t0();
const {
0: [n1],
} = children(root);
watchEffect(() => { watchEffect(() => {
setText(n0, undefined, str.value); setText(n1, undefined, str.value);
}); });
return n0; return root;
}
"
`;
exports[`comile > fragment 1`] = `
"import { template, children } from 'vue/vapor';
const t0 = template(\`<p></p><span></span><div></div>\`);
export function render() {
const root = t0();
const {
0: [n1],
1: [n2],
2: [n3],
} = children(root);
return root;
} }
" "
`; `;
exports[`comile > static template 1`] = ` exports[`comile > static template 1`] = `
"import { template } from 'vue/vapor'; "import { template, children } from 'vue/vapor';
const t0 = template(\`<div><p>hello</p><input><span></span></div>\`); const t0 = template(\`<div><p>hello</p><input><span></span></div>\`);
export function render() { export function render() {
const n0 = t0(); const root = t0();
return n0; const {
0: [n1],
} = children(root);
return root;
} }
" "
`; `;

View File

@ -4,7 +4,7 @@ exports[`fixtures 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { watchEffect } from 'vue' import { watchEffect } from 'vue'
import { template, children, insert, setText, on, setHtml } from 'vue/vapor' import { template, children, insert, setText, on, setHtml } from 'vue/vapor'
const t0 = template(\`<div><h1 id=\\"title\\">Counter</h1><p>Count: </p><p>Double: </p><button>Increment</button><div></div><input type=\\"text\\"><p>once: </p><p>{{ count }}</p></div>\`) const t0 = template(\`<h1 id=\\"title\\">Counter</h1><p>Count: </p><p>Double: </p><button>Increment</button><div></div><input type=\\"text\\"><p>once: </p><p>{{ count }}</p>\`)
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
const html = '<b>HTML</b>' const html = '<b>HTML</b>'
@ -19,28 +19,28 @@ const increment = () => count.value++
return (() => { return (() => {
const n0 = t0() const root = t0()
const { 1: [n1], 2: [n3], 3: [n5], 4: [n6], 6: [n7],} = children(n0) const { 0: [n1], 1: [n2], 2: [n4], 3: [n6], 4: [n7], 5: [n8], 6: [n9], 7: [n11],} = children(root)
const n2 = document.createTextNode(count.value) const n3 = document.createTextNode(count.value)
insert(n2, n1) insert(n3, n2)
const n4 = document.createTextNode(double.value) const n5 = document.createTextNode(double.value)
insert(n4, n3) insert(n5, n4)
const n8 = document.createTextNode(count.value) const n10 = document.createTextNode(count.value)
insert(n8, n7) insert(n10, n9)
setText(n8, undefined, count.value) setText(n10, undefined, count.value)
watchEffect(() => { watchEffect(() => {
setText(n2, undefined, count.value) setText(n3, undefined, count.value)
}) })
watchEffect(() => { watchEffect(() => {
setText(n4, undefined, double.value) setText(n5, undefined, double.value)
}) })
watchEffect(() => { watchEffect(() => {
on(n5, \\"click\\", increment) on(n6, \\"click\\", increment)
}) })
watchEffect(() => { watchEffect(() => {
setHtml(n6, undefined, html) setHtml(n7, undefined, html)
}) })
return n0 return root
})(); })();
} }

View File

@ -28,6 +28,11 @@ describe('comile', () => {
expect(code).matchSnapshot() expect(code).matchSnapshot()
}) })
test('fragment', async () => {
const code = await compile(`<p/><span/><div/>`)
expect(code).matchSnapshot()
})
test('bindings', async () => { test('bindings', async () => {
const code = await compile(`<div>count is {{ count }}.</div>`, { const code = await compile(`<div>count is {{ count }}.</div>`, {
bindingMetadata: { bindingMetadata: {

View File

@ -10,7 +10,6 @@ const html = '<b>HTML</b>'
</script> </script>
<template> <template>
<div>
<h1 id="title">Counter</h1> <h1 id="title">Counter</h1>
<p>Count: {{ count }}</p> <p>Count: {{ count }}</p>
<p>Double: {{ double }}</p> <p>Double: {{ double }}</p>
@ -19,5 +18,4 @@ const html = '<b>HTML</b>'
<input type="text" /> <input type="text" />
<p v-once>once: {{ count }}</p> <p v-once>once: {{ count }}</p>
<p v-pre>{{ count }}</p> <p v-pre>{{ count }}</p>
</div>
</template> </template>

View File

@ -27,18 +27,14 @@ export function generate(
vaporHelpers.add('template') vaporHelpers.add('template')
} }
for (const [, { id, children }] of Object.entries(ir.children)) { {
code += `const n${id} = t0()\n` code += `const root = t0()\n`
code += `const {${genChildren(ir.children.children)}} = children(root)\n`
if (Object.keys(children).length) {
code += `const {${genChildren(children)}} = children(n${id})\n`
vaporHelpers.add('children') vaporHelpers.add('children')
}
for (const operation of ir.operation) { for (const operation of ir.operation) {
code += genOperation(operation) code += genOperation(operation)
} }
for (const [_expr, operations] of Object.entries(ir.effect)) { for (const [_expr, operations] of Object.entries(ir.effect)) {
// TODO don't use watchEffect from vue/core, implement `effect` function in runtime-vapor package // TODO don't use watchEffect from vue/core, implement `effect` function in runtime-vapor package
let scope = `watchEffect(() => {\n` let scope = `watchEffect(() => {\n`
@ -49,9 +45,8 @@ export function generate(
scope += '})\n' scope += '})\n'
code += scope code += scope
} }
// TODO multiple-template // TODO multiple-template
code += `return n${id}\n` code += `return root\n`
} }
if (vaporHelpers.size) if (vaporHelpers.size)

View File

@ -20,7 +20,7 @@ export interface IRNode {
export interface RootIRNode extends IRNode { export interface RootIRNode extends IRNode {
type: IRNodeTypes.ROOT type: IRNodeTypes.ROOT
template: Array<TemplateGeneratorIRNode> template: Array<TemplateGeneratorIRNode>
children: DynamicChildren children: DynamicChild
// TODO multi-expression effect // TODO multi-expression effect
effect: Record<string /* expr */, OperationNode[]> effect: Record<string /* expr */, OperationNode[]>
operation: OperationNode[] operation: OperationNode[]

View File

@ -135,17 +135,22 @@ export function transform(
type: IRNodeTypes.ROOT, type: IRNodeTypes.ROOT,
loc: root.loc, loc: root.loc,
template: [], template: [],
children: {}, children: {} as any,
effect: Object.create(null), effect: Object.create(null),
operation: [], operation: [],
helpers: new Set([]), helpers: new Set([]),
vaporHelpers: new Set([]), vaporHelpers: new Set([]),
} }
const ctx = createRootContext(ir, root, options) const ctx = createRootContext(ir, root, options)
const rootId = ctx.getElementId()
// TODO: transform presets, see packages/compiler-core/src/transforms // TODO: transform presets, see packages/compiler-core/src/transforms
transformChildren(ctx, true) transformChildren(ctx, true)
ir.children = ctx.children ir.children = {
store: true,
id: rootId,
children: ctx.children,
}
return ir return ir
} }

View File

@ -7,7 +7,7 @@ import {
import { isArray } from '@vue/shared' import { isArray } from '@vue/shared'
export type Block = Node | Fragment | Block[] export type Block = Node | Fragment | Block[]
export type Fragment = { nodes: Block; anchor?: Node } export type Fragment = { nodes: Block; anchor: Node }
export type BlockFn = (props?: any) => Block export type BlockFn = (props?: any) => Block
export function render( export function render(
@ -52,7 +52,7 @@ export function insert(
for (const child of block) insert(child, parent, anchor) for (const child of block) insert(child, parent, anchor)
} else { } else {
insert(block.nodes, parent, anchor) insert(block.nodes, parent, anchor)
block.anchor && parent.insertBefore(block.anchor, anchor) parent.insertBefore(block.anchor, anchor)
} }
// } // }
} }

View File

@ -1,6 +1,6 @@
export const template = (str: string): (() => Node) => { export const template = (str: string): (() => Node) => {
let cached = false let cached = false
let node: Node let node: DocumentFragment
return () => { return () => {
if (!cached) { if (!cached) {
cached = true cached = true
@ -9,7 +9,7 @@ export const template = (str: string): (() => Node) => {
// first render: insert the node directly. // first render: insert the node directly.
// this removes it from the template fragment to avoid keeping two copies // this removes it from the template fragment to avoid keeping two copies
// of the inserted tree in memory, even if the template is used only once. // of the inserted tree in memory, even if the template is used only once.
return (node = t.content.firstChild!) return (node = t.content)
} else { } else {
// repeated renders: clone from cache. This is more performant and // repeated renders: clone from cache. This is more performant and
// efficient when dealing with big lists where the template is repeated // efficient when dealing with big lists where the template is repeated

View File

@ -0,0 +1,4 @@
<template>
<p>hello</p>
<p>world</p>
</template>

View File

@ -5,7 +5,7 @@ const mod = (modules['.' + location.pathname] || modules['./App.vue'])()
mod.then(({ default: m }) => { mod.then(({ default: m }) => {
render(() => { render(() => {
const returned = m.setup({}, { expose() {} }) const returned = m.setup?.({}, { expose() {} })
return m.render(returned) return m.render(returned)
}, '#app') }, '#app')
}) })