From ac686033aa3cc2f545b033ee70155680e45809e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Sun, 26 Nov 2023 02:13:59 +0800 Subject: [PATCH] feat: fragment --- README.md | 1 + .../__snapshots__/compile.test.ts.snap | 141 ++++++++++++------ .../__snapshots__/fixtures.test.ts.snap | 30 ++-- .../compiler-vapor/__tests__/compile.test.ts | 5 + .../__tests__/fixtures/counter.vue | 18 +-- packages/compiler-vapor/src/generate.ts | 15 +- packages/compiler-vapor/src/ir.ts | 2 +- packages/compiler-vapor/src/transform.ts | 9 +- packages/runtime-vapor/src/render.ts | 4 +- packages/runtime-vapor/src/template.ts | 4 +- playground/src/App-fragment.vue | 4 + playground/src/main.ts | 2 +- 12 files changed, 146 insertions(+), 89 deletions(-) create mode 100644 playground/src/App-fragment.vue diff --git a/README.md b/README.md index 018a33d17..f6342487a 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ See the To-do list below or `// TODO` comments in code (`compiler-vapor` and `ru - [ ] compiler - [ ] Remove DOM API in codegen - [ ] Fragment + - [x] multiple root nodes - [ ] Built-in Components - [ ] Transition - [ ] TransitionGroup diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap index 43bd84de0..3ec2aa6ab 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap @@ -5,86 +5,106 @@ exports[`comile > bindings 1`] = ` import { template, children, insert, setText } from 'vue/vapor'; const t0 = template(\`
count is .
\`); export function render() { - const n0 = t0(); + const root = t0(); const { - 1: [n2], - } = children(n0); - const n1 = document.createTextNode(count.value); - insert(n1, n0, n2); + 0: [ + n1, + { + 1: [n3], + }, + ], + } = children(root); + const n2 = document.createTextNode(count.value); + insert(n2, n1, n3); watchEffect(() => { - setText(n1, undefined, count.value); + setText(n2, undefined, count.value); }); - return n0; + return root; } " `; exports[`comile > directives > v-bind > simple expression 1`] = ` "import { watchEffect } from 'vue'; -import { template, setAttr } from 'vue/vapor'; +import { template, children, setAttr } from 'vue/vapor'; const t0 = template(\`
\`); export function render() { - const n0 = t0(); + const root = t0(); + const { + 0: [n1], + } = children(root); 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`] = ` "import { watchEffect } from 'vue'; -import { template, setHtml } from 'vue/vapor'; +import { template, children, setHtml } from 'vue/vapor'; const t0 = template(\`
\`); export function render() { - const n0 = t0(); + const root = t0(); + const { + 0: [n1], + } = children(root); watchEffect(() => { - setHtml(n0, undefined, ''); + setHtml(n1, undefined, ''); }); - return n0; + return root; } " `; exports[`comile > directives > v-html > simple expression 1`] = ` "import { watchEffect } from 'vue'; -import { template, setHtml } from 'vue/vapor'; +import { template, children, setHtml } from 'vue/vapor'; const t0 = template(\`
\`); export function render() { - const n0 = t0(); + const root = t0(); + const { + 0: [n1], + } = children(root); watchEffect(() => { - setHtml(n0, undefined, code.value); + setHtml(n1, undefined, code.value); }); - return n0; + return root; } " `; exports[`comile > directives > v-on > simple expression 1`] = ` "import { watchEffect } from 'vue'; -import { template, on } from 'vue/vapor'; +import { template, children, on } from 'vue/vapor'; const t0 = template(\`
\`); export function render() { - const n0 = t0(); + const root = t0(); + const { + 0: [n1], + } = children(root); watchEffect(() => { - on(n0, 'click', handleClick); + on(n1, 'click', handleClick); }); - return n0; + return root; } " `; exports[`comile > directives > v-once > as root node 1`] = ` "import { watchEffect } from 'vue'; -import { template, setAttr } from 'vue/vapor'; +import { template, children, setAttr } from 'vue/vapor'; const t0 = template(\`
\`); export function render() { - const n0 = t0(); + const root = t0(); + const { + 0: [n1], + } = children(root); 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'; const t0 = template(\`
\`); export function render() { - const n0 = t0(); + const root = t0(); const { - 1: [n2], - } = children(n0); - const n1 = document.createTextNode(msg.value); - insert(n1, n0, 0 /* InsertPosition.FIRST */); - setText(n1, undefined, msg.value); - setAttr(n2, 'class', undefined, clz.value); - return n0; + 0: [ + n1, + { + 1: [n3], + }, + ], + } = 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`] = ` "import { watchEffect } from 'vue'; -import { template, setText } from 'vue/vapor'; +import { template, children, setText } from 'vue/vapor'; const t0 = template(\`
\`); export function render() { - const n0 = t0(); + const root = t0(); + const { + 0: [n1], + } = children(root); watchEffect(() => { - setText(n0, undefined, ''); + setText(n1, undefined, ''); }); - return n0; + return root; } " `; exports[`comile > directives > v-text > simple expression 1`] = ` "import { watchEffect } from 'vue'; -import { template, setText } from 'vue/vapor'; +import { template, children, setText } from 'vue/vapor'; const t0 = template(\`
\`); export function render() { - const n0 = t0(); + const root = t0(); + const { + 0: [n1], + } = children(root); 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(\`

\`); +export function render() { + const root = t0(); + const { + 0: [n1], + 1: [n2], + 2: [n3], + } = children(root); + return root; } " `; exports[`comile > static template 1`] = ` -"import { template } from 'vue/vapor'; +"import { template, children } from 'vue/vapor'; const t0 = template(\`

hello

\`); export function render() { - const n0 = t0(); - return n0; + const root = t0(); + const { + 0: [n1], + } = children(root); + return root; } " `; diff --git a/packages/compiler-vapor/__tests__/__snapshots__/fixtures.test.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/fixtures.test.ts.snap index 6ade34c03..a95e75c53 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/fixtures.test.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/fixtures.test.ts.snap @@ -4,7 +4,7 @@ exports[`fixtures 1`] = ` "import { defineComponent as _defineComponent } from 'vue' import { watchEffect } from 'vue' import { template, children, insert, setText, on, setHtml } from 'vue/vapor' -const t0 = template(\`

Counter

Count:

Double:

once:

{{ count }}

\`) +const t0 = template(\`

Counter

Count:

Double:

once:

{{ count }}

\`) import { ref, computed } from 'vue' const html = 'HTML' @@ -19,28 +19,28 @@ const increment = () => count.value++ return (() => { -const n0 = t0() -const { 1: [n1], 2: [n3], 3: [n5], 4: [n6], 6: [n7],} = children(n0) -const n2 = document.createTextNode(count.value) -insert(n2, n1) -const n4 = document.createTextNode(double.value) -insert(n4, n3) -const n8 = document.createTextNode(count.value) -insert(n8, n7) -setText(n8, undefined, count.value) +const root = t0() +const { 0: [n1], 1: [n2], 2: [n4], 3: [n6], 4: [n7], 5: [n8], 6: [n9], 7: [n11],} = children(root) +const n3 = document.createTextNode(count.value) +insert(n3, n2) +const n5 = document.createTextNode(double.value) +insert(n5, n4) +const n10 = document.createTextNode(count.value) +insert(n10, n9) +setText(n10, undefined, count.value) watchEffect(() => { -setText(n2, undefined, count.value) +setText(n3, undefined, count.value) }) watchEffect(() => { -setText(n4, undefined, double.value) +setText(n5, undefined, double.value) }) watchEffect(() => { -on(n5, \\"click\\", increment) +on(n6, \\"click\\", increment) }) watchEffect(() => { -setHtml(n6, undefined, html) +setHtml(n7, undefined, html) }) -return n0 +return root })(); } diff --git a/packages/compiler-vapor/__tests__/compile.test.ts b/packages/compiler-vapor/__tests__/compile.test.ts index 2c1eacca5..fb17bd0c8 100644 --- a/packages/compiler-vapor/__tests__/compile.test.ts +++ b/packages/compiler-vapor/__tests__/compile.test.ts @@ -28,6 +28,11 @@ describe('comile', () => { expect(code).matchSnapshot() }) + test('fragment', async () => { + const code = await compile(`

`) + expect(code).matchSnapshot() + }) + test('bindings', async () => { const code = await compile(`
count is {{ count }}.
`, { bindingMetadata: { diff --git a/packages/compiler-vapor/__tests__/fixtures/counter.vue b/packages/compiler-vapor/__tests__/fixtures/counter.vue index d09cfd2e0..c780b459d 100644 --- a/packages/compiler-vapor/__tests__/fixtures/counter.vue +++ b/packages/compiler-vapor/__tests__/fixtures/counter.vue @@ -10,14 +10,12 @@ const html = 'HTML' diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index b14c98407..a21860373 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -27,18 +27,14 @@ export function generate( vaporHelpers.add('template') } - for (const [, { id, children }] of Object.entries(ir.children)) { - code += `const n${id} = t0()\n` - - if (Object.keys(children).length) { - code += `const {${genChildren(children)}} = children(n${id})\n` - vaporHelpers.add('children') - } + { + code += `const root = t0()\n` + code += `const {${genChildren(ir.children.children)}} = children(root)\n` + vaporHelpers.add('children') for (const operation of ir.operation) { code += genOperation(operation) } - for (const [_expr, operations] of Object.entries(ir.effect)) { // TODO don't use watchEffect from vue/core, implement `effect` function in runtime-vapor package let scope = `watchEffect(() => {\n` @@ -49,9 +45,8 @@ export function generate( scope += '})\n' code += scope } - // TODO multiple-template - code += `return n${id}\n` + code += `return root\n` } if (vaporHelpers.size) diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 7e0e8da05..a5c60056d 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -20,7 +20,7 @@ export interface IRNode { export interface RootIRNode extends IRNode { type: IRNodeTypes.ROOT template: Array - children: DynamicChildren + children: DynamicChild // TODO multi-expression effect effect: Record operation: OperationNode[] diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 3623766ae..797833db9 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -135,17 +135,22 @@ export function transform( type: IRNodeTypes.ROOT, loc: root.loc, template: [], - children: {}, + children: {} as any, effect: Object.create(null), operation: [], helpers: new Set([]), vaporHelpers: new Set([]), } const ctx = createRootContext(ir, root, options) + const rootId = ctx.getElementId() // TODO: transform presets, see packages/compiler-core/src/transforms transformChildren(ctx, true) - ir.children = ctx.children + ir.children = { + store: true, + id: rootId, + children: ctx.children, + } return ir } diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index b155f8b09..add59f46b 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -7,7 +7,7 @@ import { import { isArray } from '@vue/shared' 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 function render( @@ -52,7 +52,7 @@ export function insert( for (const child of block) insert(child, parent, anchor) } else { insert(block.nodes, parent, anchor) - block.anchor && parent.insertBefore(block.anchor, anchor) + parent.insertBefore(block.anchor, anchor) } // } } diff --git a/packages/runtime-vapor/src/template.ts b/packages/runtime-vapor/src/template.ts index 4f66b1572..1f02b32c3 100644 --- a/packages/runtime-vapor/src/template.ts +++ b/packages/runtime-vapor/src/template.ts @@ -1,6 +1,6 @@ export const template = (str: string): (() => Node) => { let cached = false - let node: Node + let node: DocumentFragment return () => { if (!cached) { cached = true @@ -9,7 +9,7 @@ export const template = (str: string): (() => Node) => { // first render: insert the node directly. // 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. - return (node = t.content.firstChild!) + return (node = t.content) } else { // repeated renders: clone from cache. This is more performant and // efficient when dealing with big lists where the template is repeated diff --git a/playground/src/App-fragment.vue b/playground/src/App-fragment.vue new file mode 100644 index 000000000..a1fea15e7 --- /dev/null +++ b/playground/src/App-fragment.vue @@ -0,0 +1,4 @@ + diff --git a/playground/src/main.ts b/playground/src/main.ts index 261042c33..06b2c60ad 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -5,7 +5,7 @@ const mod = (modules['.' + location.pathname] || modules['./App.vue'])() mod.then(({ default: m }) => { render(() => { - const returned = m.setup({}, { expose() {} }) + const returned = m.setup?.({}, { expose() {} }) return m.render(returned) }, '#app') })