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 @@
+
+ 1{{ 2 }}{{ 3 }}4
+ div
+
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