diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap index 4fa319bd1..020a7f31e 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap @@ -8,16 +8,16 @@ export function render() { const n0 = t0(); const { 0: [ - n1, + n3, { - 1: [n3], + 1: [n2], }, ], } = children(n0); - const n2 = createTextNode(count.value); - insert(n2, n1, n3); + const n1 = createTextNode(count.value); + insert(n1, n3, n2); watchEffect(() => { - setText(n2, undefined, count.value); + setText(n1, undefined, count.value); }); return n0; } @@ -110,22 +110,22 @@ export function render() { `; exports[`comile > directives > v-once > basic 1`] = ` -"import { template, children, createTextNode, insert, setText, setAttr } from 'vue/vapor'; +"import { template, children, createTextNode, setText, setAttr, insert } from 'vue/vapor'; const t0 = template('
'); export function render() { const n0 = t0(); const { 0: [ - n1, + n3, { - 1: [n3], + 2: [n2], }, ], } = children(n0); - const n2 = createTextNode(msg.value); - insert(n2, n1, 0 /* InsertPosition.FIRST */); - setText(n2, undefined, msg.value); - setAttr(n3, 'class', undefined, clz.value); + const n1 = createTextNode(msg.value); + setText(n1, undefined, msg.value); + setAttr(n2, 'class', undefined, clz.value); + insert(n1, n3, 0 /* InsertPosition.FIRST */); return n0; } " @@ -167,14 +167,13 @@ export function render() { exports[`comile > dynamic root 1`] = ` "import { watchEffect } from 'vue'; -import { fragment, createTextNode, insert, setText } from 'vue/vapor'; +import { fragment, createTextNode, append, setText } from 'vue/vapor'; export function render() { const t0 = fragment(); const n0 = t0(); const n1 = createTextNode(1); - insert(n1, n0, 0 /* InsertPosition.FIRST */); const n2 = createTextNode(2); - insert(n2, n0); + append(n0, n1, n2); watchEffect(() => { setText(n1, undefined, 1); }); @@ -203,8 +202,8 @@ const t0 = template('2'); export function render() { const n0 = t0(); const n1 = createTextNode(1); - insert(n1, n0, 0 /* InsertPosition.FIRST */); const n2 = createTextNode(3); + insert(n1, n0, 0 /* InsertPosition.FIRST */); insert(n2, n0); watchEffect(() => { setText(n1, undefined, 1); diff --git a/packages/compiler-vapor/__tests__/__snapshots__/fixtures.test.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/fixtures.test.ts.snap index 0c20d725a..323534710 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/fixtures.test.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/fixtures.test.ts.snap @@ -20,19 +20,19 @@ const increment = () => count.value++ return (() => { const n0 = t0() -const { 1: [n1], 2: [n3], 3: [n5], 4: [n6], 6: [n7],} = children(n0) -const n2 = createTextNode(count.value) -insert(n2, n1) -const n4 = createTextNode(double.value) -insert(n4, n3) -const n8 = createTextNode(count.value) -insert(n8, n7) -setText(n8, undefined, count.value) +const { 1: [n2], 2: [n4], 3: [n5], 4: [n6], 6: [n8],} = children(n0) +const n1 = createTextNode(count.value) +insert(n1, n2) +const n3 = createTextNode(double.value) +insert(n3, n4) +const n7 = createTextNode(count.value) +setText(n7, undefined, count.value) +insert(n7, n8) watchEffect(() => { -setText(n2, undefined, count.value) +setText(n1, undefined, count.value) }) watchEffect(() => { -setText(n4, undefined, double.value) +setText(n3, undefined, double.value) }) watchEffect(() => { on(n5, \\"click\\", increment) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index f1d20ab0b..d101fcf69 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -33,11 +33,11 @@ export function generate( }) { - code += `const n${ir.children.id} = t0()\n` - if (Object.keys(ir.children.children).length) { - code += `const {${genChildren(ir.children.children)}} = children(n${ - ir.children.id - })\n` + code += `const n${ir.dynamic.id} = t0()\n` + + const children = genChildren(ir.dynamic.children) + if (children) { + code += `const ${children} = children(n${ir.dynamic.id})\n` vaporHelpers.add('children') } @@ -56,7 +56,7 @@ export function generate( } // TODO multiple-template // TODO return statement in IR - code += `return n${ir.children.id}\n` + code += `return n${ir.dynamic.id}\n` } if (vaporHelpers.size) @@ -79,59 +79,65 @@ export function generate( preamble, } - function genOperation(operation: OperationNode) { + function genOperation(oper: OperationNode) { let code = '' // TODO: cache old value - switch (operation.type) { + switch (oper.type) { case IRNodeTypes.SET_PROP: { - code = `setAttr(n${operation.element}, ${JSON.stringify( - operation.name, - )}, undefined, ${operation.value})\n` + code = `setAttr(n${oper.element}, ${JSON.stringify( + oper.name, + )}, undefined, ${oper.value})\n` vaporHelpers.add('setAttr') break } case IRNodeTypes.SET_TEXT: { - code = `setText(n${operation.element}, undefined, ${operation.value})\n` + code = `setText(n${oper.element}, undefined, ${oper.value})\n` vaporHelpers.add('setText') break } case IRNodeTypes.SET_EVENT: { - code = `on(n${operation.element}, ${JSON.stringify(operation.name)}, ${ - operation.value + code = `on(n${oper.element}, ${JSON.stringify(oper.name)}, ${ + oper.value })\n` vaporHelpers.add('on') break } case IRNodeTypes.SET_HTML: { - code = `setHtml(n${operation.element}, undefined, ${operation.value})\n` + code = `setHtml(n${oper.element}, undefined, ${oper.value})\n` vaporHelpers.add('setHtml') break } case IRNodeTypes.CREATE_TEXT_NODE: { - code = `const n${operation.id} = createTextNode(${operation.value})\n` + code = `const n${oper.id} = createTextNode(${oper.value})\n` vaporHelpers.add('createTextNode') break } case IRNodeTypes.INSERT_NODE: { let anchor = '' - if (typeof operation.anchor === 'number') { - anchor = `, n${operation.anchor}` - } else if (operation.anchor === 'first') { + if (typeof oper.anchor === 'number') { + anchor = `, n${oper.anchor}` + } else if (oper.anchor === 'first') { anchor = `, 0 /* InsertPosition.FIRST */` } - code = `insert(n${operation.element}, n${operation.parent}${anchor})\n` + code = `insert(n${oper.element}, n${oper.parent}${anchor})\n` vaporHelpers.add('insert') break } - + case IRNodeTypes.APPEND_NODE: { + code = `append(n${oper.parent}, ${oper.elements + .map((el) => `n${el}`) + .join(', ')})\n` + vaporHelpers.add('append') + break + } default: - checkNever(operation) + checkNever(oper) } return code @@ -139,16 +145,26 @@ export function generate( } function genChildren(children: DynamicChildren) { - let str = '' + let code = '' + // TODO + let offset = 0 + for (const [index, child] of Object.entries(children)) { - str += ` ${index}: [` - if (child.store) { - str += `n${child.id}` - } - if (Object.keys(child.children).length) { - str += `, {${genChildren(child.children)}}` - } - str += '],' + const childrenLength = Object.keys(child.children).length + if (child.ghost && child.placeholder === null && childrenLength === 0) + continue + + code += ` ${Number(index) + offset}: [` + + const id = child.ghost ? child.placeholder : child.id + if (id !== null) code += `n${id}` + + const childrenString = childrenLength && genChildren(child.children) + if (childrenString) code += `, ${childrenString}` + + code += '],' } - return str + + if (!code) return '' + return `{${code}}` } diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index d69e522b4..bd4909fe5 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -11,6 +11,7 @@ export const enum IRNodeTypes { SET_HTML, INSERT_NODE, + APPEND_NODE, CREATE_TEXT_NODE, } @@ -22,7 +23,7 @@ export interface IRNode { export interface RootIRNode extends IRNode { type: IRNodeTypes.ROOT template: Array - children: DynamicChild + dynamic: DynamicInfo // TODO multi-expression effect effect: Record operation: OperationNode[] @@ -71,11 +72,18 @@ export interface CreateTextNodeIRNode extends IRNode { value: string } +export type InsertAnchor = number | 'first' | 'last' export interface InsertNodeIRNode extends IRNode { type: IRNodeTypes.INSERT_NODE element: number parent: number - anchor: number | 'first' | 'last' + anchor: InsertAnchor +} + +export interface AppendNodeIRNode extends IRNode { + type: IRNodeTypes.APPEND_NODE + elements: number[] + parent: number } export type OperationNode = @@ -85,11 +93,14 @@ export type OperationNode = | SetHtmlIRNode | CreateTextNodeIRNode | InsertNodeIRNode + | AppendNodeIRNode -export interface DynamicChild { +export interface DynamicInfo { id: number | null - store: boolean + referenced: boolean + /** created by DOM API */ ghost: boolean + placeholder: number | null children: DynamicChildren } -export type DynamicChildren = Record +export type DynamicChildren = Record diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 00e9de330..a5dee2a35 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -11,10 +11,11 @@ import type { ExpressionNode, } from '@vue/compiler-dom' import { - type DynamicChildren, type OperationNode, type RootIRNode, IRNodeTypes, + DynamicInfo, + InsertAnchor, } from './ir' import { isVoidTag } from '@vue/shared' @@ -24,14 +25,13 @@ export interface TransformContext { root: TransformContext index: number options: TransformOptions - template: string - children: DynamicChildren - store: boolean - ghost: boolean - once: boolean - id: number | null - getId(): number + template: string + dynamic: DynamicInfo + + once: boolean + + reference(): number incraseId(): number registerTemplate(): number registerEffect(expr: string, operation: OperationNode): void @@ -53,18 +53,19 @@ function createRootContext( index: 0, root: undefined as any, // set later options, - children: {}, - store: false, - ghost: false, + dynamic: ir.dynamic, once: false, - id: null, incraseId: () => globalId++, - getId() { - if (this.id !== null) return this.id - return (this.id = this.incraseId()) + reference() { + if (this.dynamic.id !== null) return this.dynamic.id + this.dynamic.referenced = true + return (this.dynamic.id = this.incraseId()) }, registerEffect(expr, operation) { + if (this.once) { + return this.registerOpration(operation) + } if (!effect[expr]) effect[expr] = [] effect[expr].push(operation) }, @@ -97,6 +98,7 @@ function createRootContext( }, } ctx.root = ctx + ctx.reference() return ctx } @@ -105,27 +107,19 @@ function createContext( parent: TransformContext, index: number, ): TransformContext { - const children = {} - const ctx: TransformContext = { ...parent, - id: null, node, parent, index, - get template() { - return parent.template - }, - set template(t) { - parent.template = t - }, - children, - store: false, - registerEffect(expr, operation) { - if (ctx.once) { - return ctx.registerOpration(operation) - } - return parent.registerEffect(expr, operation) + + template: '', + dynamic: { + id: null, + referenced: false, + ghost: false, + placeholder: null, + children: {}, }, } return ctx @@ -140,23 +134,22 @@ export function transform( type: IRNodeTypes.ROOT, loc: root.loc, template: [], - children: {} as any, + dynamic: { + id: null, + referenced: true, + ghost: true, + placeholder: null, + children: {}, + }, effect: Object.create(null), operation: [], helpers: new Set([]), vaporHelpers: new Set([]), } const ctx = createRootContext(ir, root, options) - const rootId = ctx.getId() // TODO: transform presets, see packages/compiler-core/src/transforms transformChildren(ctx, true) - ir.children = { - id: rootId, - store: true, - ghost: false, - children: ctx.children, - } if (ir.template.length === 0) { ir.template.push({ type: IRNodeTypes.FRAGMENT_FACTORY, @@ -174,15 +167,57 @@ function transformChildren( const { node: { children }, } = ctx - let index = 0 + const childrenTemplate: string[] = [] children.forEach((child, i) => walkNode(child, i)) + const dynamicChildren = Object.values(ctx.dynamic.children) + const dynamicCount = dynamicChildren.reduce( + (prev, child) => prev + (child.ghost ? 1 : 0), + 0, + ) + if (dynamicCount === children.length) { + // all dynamic node + ctx.registerOpration({ + type: IRNodeTypes.APPEND_NODE, + loc: ctx.node.loc, + elements: dynamicChildren.map((child) => child.id!), + parent: ctx.reference(), + }) + } else if (dynamicCount > 0 && dynamicCount < children.length) { + // mixed + for (const [indexString, child] of Object.entries(ctx.dynamic.children)) { + if (!child.ghost) continue + + const index = Number(indexString) + let anchor: InsertAnchor + if (index === 0) { + anchor = 'first' + } else if (index === children.length - 1) { + anchor = 'last' + } else { + childrenTemplate[index] = `` + anchor = child.placeholder = ctx.incraseId() + } + + ctx.registerOpration({ + type: IRNodeTypes.INSERT_NODE, + loc: ctx.node.loc, + element: child.id!, + parent: ctx.reference(), + anchor, + }) + } + } + + ctx.template += childrenTemplate.join('') + + // finalize template if (root) ctx.registerTemplate() - function walkNode(node: TemplateChildNode, i: number) { + function walkNode(node: TemplateChildNode, index: number) { const child = createContext(node, ctx, index) - const isFirst = i === 0 - const isLast = i === children.length - 1 + const isFirst = index === 0 + const isLast = index === children.length - 1 switch (node.type) { case 1 satisfies NodeTypes.ELEMENT: { @@ -190,11 +225,11 @@ function transformChildren( break } case 2 satisfies NodeTypes.TEXT: { - ctx.template += node.content + child.template += node.content break } case 3 satisfies NodeTypes.COMMENT: { - ctx.template += `` + child.template += `` break } case 5 satisfies NodeTypes.INTERPOLATION: { @@ -214,19 +249,20 @@ function transformChildren( // IfNode // IfBranchNode // ForNode - ctx.template += `[type: ${node.type}]` + child.template += `[type: ${node.type}]` } } - if (Object.keys(child.children).length > 0 || child.store) - ctx.children[index] = { - id: child.store ? child.getId() : null, - store: child.store, - children: child.children, - ghost: child.ghost, - } + childrenTemplate.push(child.template) - if (!child.ghost) index++ + if ( + child.dynamic.ghost || + child.dynamic.referenced || + child.dynamic.placeholder || + Object.keys(child.dynamic.children).length + ) { + ctx.dynamic.children[index] = child.dynamic + } } } @@ -254,62 +290,38 @@ function transformInterpolation( ) { const { node } = ctx - if (node.content.type === (4 satisfies NodeTypes.SIMPLE_EXPRESSION)) { - const expr = processExpression(ctx, node.content)! - - const parent = ctx.parent! - const parentId = parent.getId() - parent.store = true - - if (isFirst && isLast) { - ctx.registerEffect(expr, { - type: IRNodeTypes.SET_TEXT, - loc: node.loc, - element: parentId, - value: expr, - }) - } else { - let id: number - let anchor: number | 'first' | 'last' - - if (!isFirst && !isLast) { - id = ctx.incraseId() - anchor = ctx.getId() - ctx.template += '' - ctx.store = true - } else { - id = ctx.getId() - ctx.ghost = true - anchor = isFirst ? 'first' : 'last' - } - - ctx.registerOpration( - { - type: IRNodeTypes.CREATE_TEXT_NODE, - loc: node.loc, - id, - value: expr, - }, - { - type: IRNodeTypes.INSERT_NODE, - loc: node.loc, - element: id, - parent: parentId, - anchor, - }, - ) - - ctx.registerEffect(expr, { - type: IRNodeTypes.SET_TEXT, - loc: node.loc, - element: id, - value: expr, - }) - } + if (node.content.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)) { + // TODO: CompoundExpressionNode: {{ count + 1 }} return } - // TODO: CompoundExpressionNode: {{ count + 1 }} + const expr = processExpression(ctx, node.content)! + + if (isFirst && isLast) { + const parent = ctx.parent! + const parentId = parent.reference() + ctx.registerEffect(expr, { + type: IRNodeTypes.SET_TEXT, + loc: node.loc, + element: parentId, + value: expr, + }) + } else { + const id = ctx.reference() + ctx.dynamic.ghost = true + ctx.registerOpration({ + type: IRNodeTypes.CREATE_TEXT_NODE, + loc: node.loc, + id, + value: expr, + }) + ctx.registerEffect(expr, { + type: IRNodeTypes.SET_TEXT, + loc: node.loc, + element: id, + value: expr, + }) + } } function transformProp( @@ -327,7 +339,6 @@ function transformProp( return } - ctx.store = true const expr = processExpression(ctx, node.exp) switch (name) { case 'bind': { @@ -348,7 +359,7 @@ function transformProp( ctx.registerEffect(expr, { type: IRNodeTypes.SET_PROP, loc: node.loc, - element: ctx.getId(), + element: ctx.reference(), name: node.arg.content, value: expr, }) @@ -372,7 +383,7 @@ function transformProp( ctx.registerEffect(expr, { type: IRNodeTypes.SET_EVENT, loc: node.loc, - element: ctx.getId(), + element: ctx.reference(), name: node.arg.content, value: expr, }) @@ -383,7 +394,7 @@ function transformProp( ctx.registerEffect(value, { type: IRNodeTypes.SET_HTML, loc: node.loc, - element: ctx.getId(), + element: ctx.reference(), value, }) break @@ -393,7 +404,7 @@ function transformProp( ctx.registerEffect(value, { type: IRNodeTypes.SET_TEXT, loc: node.loc, - element: ctx.getId(), + element: ctx.reference(), value, }) break diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index 43ce1ad93..9132c2022 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -57,6 +57,10 @@ export function insert( // } } +export function append(parent: ParentNode, ...nodes: (Node | string)[]) { + parent.append(...nodes) +} + export function remove(block: Block, parent: ParentNode) { if (block instanceof Node) { parent.removeChild(block) @@ -124,6 +128,6 @@ export function children(n: ChildNode): Children { return { ...Array.from(n.childNodes).map(n => [n, children(n)]) } } -export function createTextNode(data: string): Text { - return document.createTextNode(data) +export function createTextNode(val: unknown): Text { + return document.createTextNode(toDisplayString(val)) } diff --git a/playground/src/all-dynamic.vue b/playground/src/dynamic-all.vue similarity index 100% rename from playground/src/all-dynamic.vue rename to playground/src/dynamic-all.vue diff --git a/playground/src/dynamic-mixed-mid.vue b/playground/src/dynamic-mixed-mid.vue new file mode 100644 index 000000000..6376819f2 --- /dev/null +++ b/playground/src/dynamic-mixed-mid.vue @@ -0,0 +1,4 @@ + diff --git a/playground/src/dynamic.vue b/playground/src/dynamic-mixed.vue similarity index 100% rename from playground/src/dynamic.vue rename to playground/src/dynamic-mixed.vue