feat: append multiple node

This commit is contained in:
三咲智子 Kevin Deng 2023-11-27 05:16:21 +08:00
parent a3fb85fd00
commit 07373d41fd
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
9 changed files with 222 additions and 177 deletions

View File

@ -8,16 +8,16 @@ export function render() {
const n0 = t0(); const n0 = t0();
const { const {
0: [ 0: [
n1, n3,
{ {
1: [n3], 1: [n2],
}, },
], ],
} = children(n0); } = children(n0);
const n2 = createTextNode(count.value); const n1 = createTextNode(count.value);
insert(n2, n1, n3); insert(n1, n3, n2);
watchEffect(() => { watchEffect(() => {
setText(n2, undefined, count.value); setText(n1, undefined, count.value);
}); });
return n0; return n0;
} }
@ -110,22 +110,22 @@ export function render() {
`; `;
exports[`comile > directives > v-once > basic 1`] = ` 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('<div> <span></span></div>'); const t0 = template('<div> <span></span></div>');
export function render() { export function render() {
const n0 = t0(); const n0 = t0();
const { const {
0: [ 0: [
n1, n3,
{ {
1: [n3], 2: [n2],
}, },
], ],
} = children(n0); } = children(n0);
const n2 = createTextNode(msg.value); const n1 = createTextNode(msg.value);
insert(n2, n1, 0 /* InsertPosition.FIRST */); setText(n1, undefined, msg.value);
setText(n2, undefined, msg.value); setAttr(n2, 'class', undefined, clz.value);
setAttr(n3, 'class', undefined, clz.value); insert(n1, n3, 0 /* InsertPosition.FIRST */);
return n0; return n0;
} }
" "
@ -167,14 +167,13 @@ export function render() {
exports[`comile > dynamic root 1`] = ` exports[`comile > dynamic root 1`] = `
"import { watchEffect } from 'vue'; "import { watchEffect } from 'vue';
import { fragment, createTextNode, insert, setText } from 'vue/vapor'; import { fragment, createTextNode, append, setText } from 'vue/vapor';
export function render() { export function render() {
const t0 = fragment(); const t0 = fragment();
const n0 = t0(); const n0 = t0();
const n1 = createTextNode(1); const n1 = createTextNode(1);
insert(n1, n0, 0 /* InsertPosition.FIRST */);
const n2 = createTextNode(2); const n2 = createTextNode(2);
insert(n2, n0); append(n0, n1, n2);
watchEffect(() => { watchEffect(() => {
setText(n1, undefined, 1); setText(n1, undefined, 1);
}); });
@ -203,8 +202,8 @@ const t0 = template('2');
export function render() { export function render() {
const n0 = t0(); const n0 = t0();
const n1 = createTextNode(1); const n1 = createTextNode(1);
insert(n1, n0, 0 /* InsertPosition.FIRST */);
const n2 = createTextNode(3); const n2 = createTextNode(3);
insert(n1, n0, 0 /* InsertPosition.FIRST */);
insert(n2, n0); insert(n2, n0);
watchEffect(() => { watchEffect(() => {
setText(n1, undefined, 1); setText(n1, undefined, 1);

View File

@ -20,19 +20,19 @@ const increment = () => count.value++
return (() => { return (() => {
const n0 = t0() const n0 = t0()
const { 1: [n1], 2: [n3], 3: [n5], 4: [n6], 6: [n7],} = children(n0) const { 1: [n2], 2: [n4], 3: [n5], 4: [n6], 6: [n8],} = children(n0)
const n2 = createTextNode(count.value) const n1 = createTextNode(count.value)
insert(n2, n1) insert(n1, n2)
const n4 = createTextNode(double.value) const n3 = createTextNode(double.value)
insert(n4, n3) insert(n3, n4)
const n8 = createTextNode(count.value) const n7 = createTextNode(count.value)
insert(n8, n7) setText(n7, undefined, count.value)
setText(n8, undefined, count.value) insert(n7, n8)
watchEffect(() => { watchEffect(() => {
setText(n2, undefined, count.value) setText(n1, undefined, count.value)
}) })
watchEffect(() => { watchEffect(() => {
setText(n4, undefined, double.value) setText(n3, undefined, double.value)
}) })
watchEffect(() => { watchEffect(() => {
on(n5, \\"click\\", increment) on(n5, \\"click\\", increment)

View File

@ -33,11 +33,11 @@ export function generate(
}) })
{ {
code += `const n${ir.children.id} = t0()\n` code += `const n${ir.dynamic.id} = t0()\n`
if (Object.keys(ir.children.children).length) {
code += `const {${genChildren(ir.children.children)}} = children(n${ const children = genChildren(ir.dynamic.children)
ir.children.id if (children) {
})\n` code += `const ${children} = children(n${ir.dynamic.id})\n`
vaporHelpers.add('children') vaporHelpers.add('children')
} }
@ -56,7 +56,7 @@ export function generate(
} }
// TODO multiple-template // TODO multiple-template
// TODO return statement in IR // TODO return statement in IR
code += `return n${ir.children.id}\n` code += `return n${ir.dynamic.id}\n`
} }
if (vaporHelpers.size) if (vaporHelpers.size)
@ -79,59 +79,65 @@ export function generate(
preamble, preamble,
} }
function genOperation(operation: OperationNode) { function genOperation(oper: OperationNode) {
let code = '' let code = ''
// TODO: cache old value // TODO: cache old value
switch (operation.type) { switch (oper.type) {
case IRNodeTypes.SET_PROP: { case IRNodeTypes.SET_PROP: {
code = `setAttr(n${operation.element}, ${JSON.stringify( code = `setAttr(n${oper.element}, ${JSON.stringify(
operation.name, oper.name,
)}, undefined, ${operation.value})\n` )}, undefined, ${oper.value})\n`
vaporHelpers.add('setAttr') vaporHelpers.add('setAttr')
break break
} }
case IRNodeTypes.SET_TEXT: { 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') vaporHelpers.add('setText')
break break
} }
case IRNodeTypes.SET_EVENT: { case IRNodeTypes.SET_EVENT: {
code = `on(n${operation.element}, ${JSON.stringify(operation.name)}, ${ code = `on(n${oper.element}, ${JSON.stringify(oper.name)}, ${
operation.value oper.value
})\n` })\n`
vaporHelpers.add('on') vaporHelpers.add('on')
break break
} }
case IRNodeTypes.SET_HTML: { 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') vaporHelpers.add('setHtml')
break break
} }
case IRNodeTypes.CREATE_TEXT_NODE: { 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') vaporHelpers.add('createTextNode')
break break
} }
case IRNodeTypes.INSERT_NODE: { case IRNodeTypes.INSERT_NODE: {
let anchor = '' let anchor = ''
if (typeof operation.anchor === 'number') { if (typeof oper.anchor === 'number') {
anchor = `, n${operation.anchor}` anchor = `, n${oper.anchor}`
} else if (operation.anchor === 'first') { } else if (oper.anchor === 'first') {
anchor = `, 0 /* InsertPosition.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') vaporHelpers.add('insert')
break break
} }
case IRNodeTypes.APPEND_NODE: {
code = `append(n${oper.parent}, ${oper.elements
.map((el) => `n${el}`)
.join(', ')})\n`
vaporHelpers.add('append')
break
}
default: default:
checkNever(operation) checkNever(oper)
} }
return code return code
@ -139,16 +145,26 @@ export function generate(
} }
function genChildren(children: DynamicChildren) { function genChildren(children: DynamicChildren) {
let str = '' let code = ''
// TODO
let offset = 0
for (const [index, child] of Object.entries(children)) { for (const [index, child] of Object.entries(children)) {
str += ` ${index}: [` const childrenLength = Object.keys(child.children).length
if (child.store) { if (child.ghost && child.placeholder === null && childrenLength === 0)
str += `n${child.id}` 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 += '],'
} }
if (Object.keys(child.children).length) {
str += `, {${genChildren(child.children)}}` if (!code) return ''
} return `{${code}}`
str += '],'
}
return str
} }

View File

@ -11,6 +11,7 @@ export const enum IRNodeTypes {
SET_HTML, SET_HTML,
INSERT_NODE, INSERT_NODE,
APPEND_NODE,
CREATE_TEXT_NODE, CREATE_TEXT_NODE,
} }
@ -22,7 +23,7 @@ export interface IRNode {
export interface RootIRNode extends IRNode { export interface RootIRNode extends IRNode {
type: IRNodeTypes.ROOT type: IRNodeTypes.ROOT
template: Array<TemplateFactoryIRNode | FragmentFactoryIRNode> template: Array<TemplateFactoryIRNode | FragmentFactoryIRNode>
children: DynamicChild dynamic: DynamicInfo
// TODO multi-expression effect // TODO multi-expression effect
effect: Record<string /* expr */, OperationNode[]> effect: Record<string /* expr */, OperationNode[]>
operation: OperationNode[] operation: OperationNode[]
@ -71,11 +72,18 @@ export interface CreateTextNodeIRNode extends IRNode {
value: string value: string
} }
export type InsertAnchor = number | 'first' | 'last'
export interface InsertNodeIRNode extends IRNode { export interface InsertNodeIRNode extends IRNode {
type: IRNodeTypes.INSERT_NODE type: IRNodeTypes.INSERT_NODE
element: number element: number
parent: 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 = export type OperationNode =
@ -85,11 +93,14 @@ export type OperationNode =
| SetHtmlIRNode | SetHtmlIRNode
| CreateTextNodeIRNode | CreateTextNodeIRNode
| InsertNodeIRNode | InsertNodeIRNode
| AppendNodeIRNode
export interface DynamicChild { export interface DynamicInfo {
id: number | null id: number | null
store: boolean referenced: boolean
/** created by DOM API */
ghost: boolean ghost: boolean
placeholder: number | null
children: DynamicChildren children: DynamicChildren
} }
export type DynamicChildren = Record<number, DynamicChild> export type DynamicChildren = Record<number, DynamicInfo>

View File

@ -11,10 +11,11 @@ import type {
ExpressionNode, ExpressionNode,
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { import {
type DynamicChildren,
type OperationNode, type OperationNode,
type RootIRNode, type RootIRNode,
IRNodeTypes, IRNodeTypes,
DynamicInfo,
InsertAnchor,
} from './ir' } from './ir'
import { isVoidTag } from '@vue/shared' import { isVoidTag } from '@vue/shared'
@ -24,14 +25,13 @@ export interface TransformContext<T extends Node = Node> {
root: TransformContext<RootNode> root: TransformContext<RootNode>
index: number index: number
options: TransformOptions 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 incraseId(): number
registerTemplate(): number registerTemplate(): number
registerEffect(expr: string, operation: OperationNode): void registerEffect(expr: string, operation: OperationNode): void
@ -53,18 +53,19 @@ function createRootContext(
index: 0, index: 0,
root: undefined as any, // set later root: undefined as any, // set later
options, options,
children: {}, dynamic: ir.dynamic,
store: false,
ghost: false,
once: false, once: false,
id: null,
incraseId: () => globalId++, incraseId: () => globalId++,
getId() { reference() {
if (this.id !== null) return this.id if (this.dynamic.id !== null) return this.dynamic.id
return (this.id = this.incraseId()) this.dynamic.referenced = true
return (this.dynamic.id = this.incraseId())
}, },
registerEffect(expr, operation) { registerEffect(expr, operation) {
if (this.once) {
return this.registerOpration(operation)
}
if (!effect[expr]) effect[expr] = [] if (!effect[expr]) effect[expr] = []
effect[expr].push(operation) effect[expr].push(operation)
}, },
@ -97,6 +98,7 @@ function createRootContext(
}, },
} }
ctx.root = ctx ctx.root = ctx
ctx.reference()
return ctx return ctx
} }
@ -105,27 +107,19 @@ function createContext<T extends TemplateChildNode>(
parent: TransformContext, parent: TransformContext,
index: number, index: number,
): TransformContext<T> { ): TransformContext<T> {
const children = {}
const ctx: TransformContext<T> = { const ctx: TransformContext<T> = {
...parent, ...parent,
id: null,
node, node,
parent, parent,
index, index,
get template() {
return parent.template template: '',
}, dynamic: {
set template(t) { id: null,
parent.template = t referenced: false,
}, ghost: false,
children, placeholder: null,
store: false, children: {},
registerEffect(expr, operation) {
if (ctx.once) {
return ctx.registerOpration(operation)
}
return parent.registerEffect(expr, operation)
}, },
} }
return ctx return ctx
@ -140,23 +134,22 @@ export function transform(
type: IRNodeTypes.ROOT, type: IRNodeTypes.ROOT,
loc: root.loc, loc: root.loc,
template: [], template: [],
children: {} as any, dynamic: {
id: null,
referenced: true,
ghost: true,
placeholder: null,
children: {},
},
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.getId()
// 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 = {
id: rootId,
store: true,
ghost: false,
children: ctx.children,
}
if (ir.template.length === 0) { if (ir.template.length === 0) {
ir.template.push({ ir.template.push({
type: IRNodeTypes.FRAGMENT_FACTORY, type: IRNodeTypes.FRAGMENT_FACTORY,
@ -174,15 +167,57 @@ function transformChildren(
const { const {
node: { children }, node: { children },
} = ctx } = ctx
let index = 0 const childrenTemplate: string[] = []
children.forEach((child, i) => walkNode(child, i)) 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() if (root) ctx.registerTemplate()
function walkNode(node: TemplateChildNode, i: number) { function walkNode(node: TemplateChildNode, index: number) {
const child = createContext(node, ctx, index) const child = createContext(node, ctx, index)
const isFirst = i === 0 const isFirst = index === 0
const isLast = i === children.length - 1 const isLast = index === children.length - 1
switch (node.type) { switch (node.type) {
case 1 satisfies NodeTypes.ELEMENT: { case 1 satisfies NodeTypes.ELEMENT: {
@ -190,11 +225,11 @@ function transformChildren(
break break
} }
case 2 satisfies NodeTypes.TEXT: { case 2 satisfies NodeTypes.TEXT: {
ctx.template += node.content child.template += node.content
break break
} }
case 3 satisfies NodeTypes.COMMENT: { case 3 satisfies NodeTypes.COMMENT: {
ctx.template += `<!--${node.content}-->` child.template += `<!--${node.content}-->`
break break
} }
case 5 satisfies NodeTypes.INTERPOLATION: { case 5 satisfies NodeTypes.INTERPOLATION: {
@ -214,19 +249,20 @@ function transformChildren(
// IfNode // IfNode
// IfBranchNode // IfBranchNode
// ForNode // ForNode
ctx.template += `[type: ${node.type}]` child.template += `[type: ${node.type}]`
} }
} }
if (Object.keys(child.children).length > 0 || child.store) childrenTemplate.push(child.template)
ctx.children[index] = {
id: child.store ? child.getId() : null,
store: child.store,
children: child.children,
ghost: child.ghost,
}
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,14 +290,16 @@ function transformInterpolation(
) { ) {
const { node } = ctx const { node } = ctx
if (node.content.type === (4 satisfies NodeTypes.SIMPLE_EXPRESSION)) { if (node.content.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)) {
// TODO: CompoundExpressionNode: {{ count + 1 }}
return
}
const expr = processExpression(ctx, node.content)! const expr = processExpression(ctx, node.content)!
const parent = ctx.parent!
const parentId = parent.getId()
parent.store = true
if (isFirst && isLast) { if (isFirst && isLast) {
const parent = ctx.parent!
const parentId = parent.reference()
ctx.registerEffect(expr, { ctx.registerEffect(expr, {
type: IRNodeTypes.SET_TEXT, type: IRNodeTypes.SET_TEXT,
loc: node.loc, loc: node.loc,
@ -269,36 +307,14 @@ function transformInterpolation(
value: expr, value: expr,
}) })
} else { } else {
let id: number const id = ctx.reference()
let anchor: number | 'first' | 'last' ctx.dynamic.ghost = true
ctx.registerOpration({
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, type: IRNodeTypes.CREATE_TEXT_NODE,
loc: node.loc, loc: node.loc,
id, id,
value: expr, value: expr,
}, })
{
type: IRNodeTypes.INSERT_NODE,
loc: node.loc,
element: id,
parent: parentId,
anchor,
},
)
ctx.registerEffect(expr, { ctx.registerEffect(expr, {
type: IRNodeTypes.SET_TEXT, type: IRNodeTypes.SET_TEXT,
loc: node.loc, loc: node.loc,
@ -306,10 +322,6 @@ function transformInterpolation(
value: expr, value: expr,
}) })
} }
return
}
// TODO: CompoundExpressionNode: {{ count + 1 }}
} }
function transformProp( function transformProp(
@ -327,7 +339,6 @@ function transformProp(
return return
} }
ctx.store = true
const expr = processExpression(ctx, node.exp) const expr = processExpression(ctx, node.exp)
switch (name) { switch (name) {
case 'bind': { case 'bind': {
@ -348,7 +359,7 @@ function transformProp(
ctx.registerEffect(expr, { ctx.registerEffect(expr, {
type: IRNodeTypes.SET_PROP, type: IRNodeTypes.SET_PROP,
loc: node.loc, loc: node.loc,
element: ctx.getId(), element: ctx.reference(),
name: node.arg.content, name: node.arg.content,
value: expr, value: expr,
}) })
@ -372,7 +383,7 @@ function transformProp(
ctx.registerEffect(expr, { ctx.registerEffect(expr, {
type: IRNodeTypes.SET_EVENT, type: IRNodeTypes.SET_EVENT,
loc: node.loc, loc: node.loc,
element: ctx.getId(), element: ctx.reference(),
name: node.arg.content, name: node.arg.content,
value: expr, value: expr,
}) })
@ -383,7 +394,7 @@ function transformProp(
ctx.registerEffect(value, { ctx.registerEffect(value, {
type: IRNodeTypes.SET_HTML, type: IRNodeTypes.SET_HTML,
loc: node.loc, loc: node.loc,
element: ctx.getId(), element: ctx.reference(),
value, value,
}) })
break break
@ -393,7 +404,7 @@ function transformProp(
ctx.registerEffect(value, { ctx.registerEffect(value, {
type: IRNodeTypes.SET_TEXT, type: IRNodeTypes.SET_TEXT,
loc: node.loc, loc: node.loc,
element: ctx.getId(), element: ctx.reference(),
value, value,
}) })
break break

View File

@ -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) { export function remove(block: Block, parent: ParentNode) {
if (block instanceof Node) { if (block instanceof Node) {
parent.removeChild(block) parent.removeChild(block)
@ -124,6 +128,6 @@ export function children(n: ChildNode): Children {
return { ...Array.from(n.childNodes).map(n => [n, children(n)]) } return { ...Array.from(n.childNodes).map(n => [n, children(n)]) }
} }
export function createTextNode(data: string): Text { export function createTextNode(val: unknown): Text {
return document.createTextNode(data) return document.createTextNode(toDisplayString(val))
} }

View File

@ -0,0 +1,4 @@
<template>
1{{ 2 }}{{ 3 }}4
<div>div</div>
</template>