feat: once
This commit is contained in:
parent
0b07affe0b
commit
7ddf69e6e9
|
@ -18,7 +18,7 @@ See the To-do list below or `// TODO` comments in code (`compiler-vapor` and `ru
|
||||||
- [ ] `v-model`
|
- [ ] `v-model`
|
||||||
- [ ] `v-if` / `v-else` / `v-else-if`
|
- [ ] `v-if` / `v-else` / `v-else-if`
|
||||||
- [ ] `v-for`
|
- [ ] `v-for`
|
||||||
- [ ] `v-once`
|
- [x] `v-once`
|
||||||
- [x] `v-html`
|
- [x] `v-html`
|
||||||
- [x] `v-text`
|
- [x] `v-text`
|
||||||
- [ ] `v-show`
|
- [ ] `v-show`
|
||||||
|
|
|
@ -4,7 +4,7 @@ exports[`basic 1`] = `
|
||||||
"import { defineComponent as _defineComponent } from 'vue'
|
"import { defineComponent as _defineComponent } from 'vue'
|
||||||
import { watchEffect } from 'vue'
|
import { watchEffect } from 'vue'
|
||||||
import { template, insert, setText, on, setHtml } from 'vue/vapor'
|
import { template, insert, setText, on, setHtml } from 'vue/vapor'
|
||||||
const t0 = template(\`<h1 id=\\"title\\">Counter</h1><p>Count: </p><p>Double: </p><button>Increment</button><div></div><input type=\\"text\\">\`)
|
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>\`)
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
const html = '<b>HTML</b>'
|
const html = '<b>HTML</b>'
|
||||||
|
@ -24,6 +24,9 @@ const n1 = document.createTextNode(count.value)
|
||||||
insert(n1, n0)
|
insert(n1, n0)
|
||||||
const n3 = document.createTextNode(double.value)
|
const n3 = document.createTextNode(double.value)
|
||||||
insert(n3, n2)
|
insert(n3, n2)
|
||||||
|
const n7 = document.createTextNode(count.value)
|
||||||
|
insert(n7, n6)
|
||||||
|
setText(n7, undefined, count.value)
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
setText(n1, undefined, count.value)
|
setText(n1, undefined, count.value)
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,4 +16,5 @@ const html = '<b>HTML</b>'
|
||||||
<button @click="increment">Increment</button>
|
<button @click="increment">Increment</button>
|
||||||
<div v-html="html" />
|
<div v-html="html" />
|
||||||
<input type="text" />
|
<input type="text" />
|
||||||
|
<p v-once>once: {{ count }}</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import type { CodegenOptions, CodegenResult } from '@vue/compiler-dom'
|
import type { CodegenOptions, CodegenResult } from '@vue/compiler-dom'
|
||||||
import { type DynamicChildren, type RootIRNode, IRNodeTypes } from './ir'
|
import {
|
||||||
|
type DynamicChildren,
|
||||||
|
type RootIRNode,
|
||||||
|
IRNodeTypes,
|
||||||
|
OperationNode,
|
||||||
|
} from './ir'
|
||||||
|
|
||||||
// remove when stable
|
// remove when stable
|
||||||
function checkNever(x: never): void {}
|
function checkNever(x: never): void {}
|
||||||
|
@ -30,61 +35,15 @@ export function generate(
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const operation of ir.operation) {
|
for (const operation of ir.operation) {
|
||||||
switch (operation.type) {
|
code += genOperation(operation)
|
||||||
case IRNodeTypes.TEXT_NODE: {
|
|
||||||
// TODO handle by runtime: document.createTextNode
|
|
||||||
code += `const n${operation.id} = document.createTextNode(${operation.content})\n`
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case IRNodeTypes.INSERT_NODE:
|
|
||||||
{
|
|
||||||
let anchor = ''
|
|
||||||
if (typeof operation.anchor === 'number') {
|
|
||||||
anchor = `, n${operation.anchor}`
|
|
||||||
} else if (operation.anchor === 'first') {
|
|
||||||
anchor = `, 0 /* InsertPosition.FIRST */`
|
|
||||||
}
|
|
||||||
code += `insert(n${operation.element}, n${operation.parent}${anchor})\n`
|
|
||||||
vaporHelpers.add('insert')
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [expr, effects] 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`
|
||||||
helpers.add('watchEffect')
|
helpers.add('watchEffect')
|
||||||
for (const effect of effects) {
|
for (const operation of operations) {
|
||||||
switch (effect.type) {
|
scope += genOperation(operation)
|
||||||
case IRNodeTypes.SET_PROP: {
|
|
||||||
scope += `setAttr(n${effect.element}, ${JSON.stringify(
|
|
||||||
effect.name,
|
|
||||||
)}, undefined, ${expr})\n`
|
|
||||||
vaporHelpers.add('setAttr')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case IRNodeTypes.SET_TEXT: {
|
|
||||||
scope += `setText(n${effect.element}, undefined, ${expr})\n`
|
|
||||||
vaporHelpers.add('setText')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case IRNodeTypes.SET_EVENT: {
|
|
||||||
scope += `on(n${effect.element}, ${JSON.stringify(
|
|
||||||
effect.name,
|
|
||||||
)}, ${expr})\n`
|
|
||||||
vaporHelpers.add('on')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case IRNodeTypes.SET_HTML: {
|
|
||||||
scope += `setHtml(n${effect.element}, undefined, ${expr})\n`
|
|
||||||
vaporHelpers.add('setHtml')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
checkNever(effect)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
scope += '})\n'
|
scope += '})\n'
|
||||||
code += scope
|
code += scope
|
||||||
|
@ -111,6 +70,63 @@ export function generate(
|
||||||
ast: ir as any,
|
ast: ir as any,
|
||||||
preamble,
|
preamble,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function genOperation(operation: OperationNode) {
|
||||||
|
let code = ''
|
||||||
|
|
||||||
|
switch (operation.type) {
|
||||||
|
case IRNodeTypes.SET_PROP: {
|
||||||
|
code = `setAttr(n${operation.element}, ${JSON.stringify(
|
||||||
|
operation.name,
|
||||||
|
)}, undefined, ${operation.value})\n`
|
||||||
|
vaporHelpers.add('setAttr')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case IRNodeTypes.SET_TEXT: {
|
||||||
|
code = `setText(n${operation.element}, undefined, ${operation.value})\n`
|
||||||
|
vaporHelpers.add('setText')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case IRNodeTypes.SET_EVENT: {
|
||||||
|
code = `on(n${operation.element}, ${JSON.stringify(operation.name)}, ${
|
||||||
|
operation.value
|
||||||
|
})\n`
|
||||||
|
vaporHelpers.add('on')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case IRNodeTypes.SET_HTML: {
|
||||||
|
code = `setHtml(n${operation.element}, undefined, ${operation.value})\n`
|
||||||
|
vaporHelpers.add('setHtml')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case IRNodeTypes.TEXT_NODE: {
|
||||||
|
// TODO handle by runtime: document.createTextNode
|
||||||
|
code = `const n${operation.id} = document.createTextNode(${operation.value})\n`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case IRNodeTypes.INSERT_NODE: {
|
||||||
|
let anchor = ''
|
||||||
|
if (typeof operation.anchor === 'number') {
|
||||||
|
anchor = `, n${operation.anchor}`
|
||||||
|
} else if (operation.anchor === 'first') {
|
||||||
|
anchor = `, 0 /* InsertPosition.FIRST */`
|
||||||
|
}
|
||||||
|
code = `insert(n${operation.element}, n${operation.parent}${anchor})\n`
|
||||||
|
vaporHelpers.add('insert')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
checkNever(operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
return code
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function genChildren(children: DynamicChildren) {
|
function genChildren(children: DynamicChildren) {
|
||||||
|
|
|
@ -21,7 +21,8 @@ export interface RootIRNode extends IRNode {
|
||||||
type: IRNodeTypes.ROOT
|
type: IRNodeTypes.ROOT
|
||||||
template: Array<TemplateGeneratorIRNode>
|
template: Array<TemplateGeneratorIRNode>
|
||||||
children: DynamicChildren
|
children: DynamicChildren
|
||||||
effect: Record<string, EffectNode[]>
|
// TODO multi-expression effect
|
||||||
|
effect: Record<string /* expr */, OperationNode[]>
|
||||||
operation: OperationNode[]
|
operation: OperationNode[]
|
||||||
helpers: Set<string>
|
helpers: Set<string>
|
||||||
vaporHelpers: Set<string>
|
vaporHelpers: Set<string>
|
||||||
|
@ -36,34 +37,32 @@ export interface SetPropIRNode extends IRNode {
|
||||||
type: IRNodeTypes.SET_PROP
|
type: IRNodeTypes.SET_PROP
|
||||||
element: number
|
element: number
|
||||||
name: string
|
name: string
|
||||||
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetTextIRNode extends IRNode {
|
export interface SetTextIRNode extends IRNode {
|
||||||
type: IRNodeTypes.SET_TEXT
|
type: IRNodeTypes.SET_TEXT
|
||||||
element: number
|
element: number
|
||||||
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetEventIRNode extends IRNode {
|
export interface SetEventIRNode extends IRNode {
|
||||||
type: IRNodeTypes.SET_EVENT
|
type: IRNodeTypes.SET_EVENT
|
||||||
element: number
|
element: number
|
||||||
name: string
|
name: string
|
||||||
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetHtmlIRNode extends IRNode {
|
export interface SetHtmlIRNode extends IRNode {
|
||||||
type: IRNodeTypes.SET_HTML
|
type: IRNodeTypes.SET_HTML
|
||||||
element: number
|
element: number
|
||||||
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EffectNode =
|
|
||||||
| SetPropIRNode
|
|
||||||
| SetTextIRNode
|
|
||||||
| SetEventIRNode
|
|
||||||
| SetHtmlIRNode
|
|
||||||
|
|
||||||
export interface TextNodeIRNode extends IRNode {
|
export interface TextNodeIRNode extends IRNode {
|
||||||
type: IRNodeTypes.TEXT_NODE
|
type: IRNodeTypes.TEXT_NODE
|
||||||
id: number
|
id: number
|
||||||
content: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InsertNodeIRNode extends IRNode {
|
export interface InsertNodeIRNode extends IRNode {
|
||||||
|
@ -73,7 +72,13 @@ export interface InsertNodeIRNode extends IRNode {
|
||||||
anchor: number | 'first' | 'last'
|
anchor: number | 'first' | 'last'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OperationNode = TextNodeIRNode | InsertNodeIRNode
|
export type OperationNode =
|
||||||
|
| SetPropIRNode
|
||||||
|
| SetTextIRNode
|
||||||
|
| SetEventIRNode
|
||||||
|
| SetHtmlIRNode
|
||||||
|
| TextNodeIRNode
|
||||||
|
| InsertNodeIRNode
|
||||||
|
|
||||||
export interface DynamicChild {
|
export interface DynamicChild {
|
||||||
id: number | null
|
id: number | null
|
||||||
|
|
|
@ -8,10 +8,10 @@ import type {
|
||||||
InterpolationNode,
|
InterpolationNode,
|
||||||
TransformOptions,
|
TransformOptions,
|
||||||
DirectiveNode,
|
DirectiveNode,
|
||||||
|
ExpressionNode,
|
||||||
} from '@vue/compiler-dom'
|
} from '@vue/compiler-dom'
|
||||||
import {
|
import {
|
||||||
type DynamicChildren,
|
type DynamicChildren,
|
||||||
type EffectNode,
|
|
||||||
type OperationNode,
|
type OperationNode,
|
||||||
type RootIRNode,
|
type RootIRNode,
|
||||||
IRNodeTypes,
|
IRNodeTypes,
|
||||||
|
@ -29,10 +29,11 @@ export interface TransformContext<T extends Node = Node> {
|
||||||
children: DynamicChildren
|
children: DynamicChildren
|
||||||
store: boolean
|
store: boolean
|
||||||
ghost: boolean
|
ghost: boolean
|
||||||
|
once: boolean
|
||||||
|
|
||||||
getElementId(): number
|
getElementId(): number
|
||||||
registerTemplate(): number
|
registerTemplate(): number
|
||||||
registerEffect(expr: string, effectNode: EffectNode): void
|
registerEffect(expr: string, operation: OperationNode): void
|
||||||
registerOpration(...oprations: OperationNode[]): void
|
registerOpration(...oprations: OperationNode[]): void
|
||||||
helper(name: string): string
|
helper(name: string): string
|
||||||
}
|
}
|
||||||
|
@ -54,11 +55,12 @@ function createRootContext(
|
||||||
children: {},
|
children: {},
|
||||||
store: false,
|
store: false,
|
||||||
ghost: false,
|
ghost: false,
|
||||||
|
once: false,
|
||||||
|
|
||||||
getElementId: () => i++,
|
getElementId: () => i++,
|
||||||
registerEffect(expr, effectNode) {
|
registerEffect(expr, operation) {
|
||||||
if (!effect[expr]) effect[expr] = []
|
if (!effect[expr]) effect[expr] = []
|
||||||
effect[expr].push(effectNode)
|
effect[expr].push(operation)
|
||||||
},
|
},
|
||||||
|
|
||||||
template: '',
|
template: '',
|
||||||
|
@ -115,6 +117,12 @@ function createContext<T extends TemplateChildNode>(
|
||||||
|
|
||||||
children,
|
children,
|
||||||
store: false,
|
store: false,
|
||||||
|
registerEffect(expr, operation) {
|
||||||
|
if (ctx.once) {
|
||||||
|
return ctx.registerOpration(operation)
|
||||||
|
}
|
||||||
|
parent.registerEffect(expr, operation)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
@ -230,7 +238,7 @@ function transformInterpolation(
|
||||||
const { node } = ctx
|
const { node } = ctx
|
||||||
|
|
||||||
if (node.content.type === (4 satisfies NodeTypes.SIMPLE_EXPRESSION)) {
|
if (node.content.type === (4 satisfies NodeTypes.SIMPLE_EXPRESSION)) {
|
||||||
const expr = processExpression(ctx, node.content.content)
|
const expr = processExpression(ctx, node.content)!
|
||||||
|
|
||||||
const parent = ctx.parent!
|
const parent = ctx.parent!
|
||||||
const parentId = parent.getElementId()
|
const parentId = parent.getElementId()
|
||||||
|
@ -241,6 +249,7 @@ function transformInterpolation(
|
||||||
type: IRNodeTypes.SET_TEXT,
|
type: IRNodeTypes.SET_TEXT,
|
||||||
loc: node.loc,
|
loc: node.loc,
|
||||||
element: parentId,
|
element: parentId,
|
||||||
|
value: expr,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let id: number
|
let id: number
|
||||||
|
@ -262,7 +271,7 @@ function transformInterpolation(
|
||||||
type: IRNodeTypes.TEXT_NODE,
|
type: IRNodeTypes.TEXT_NODE,
|
||||||
loc: node.loc,
|
loc: node.loc,
|
||||||
id,
|
id,
|
||||||
content: expr,
|
value: expr,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: IRNodeTypes.INSERT_NODE,
|
type: IRNodeTypes.INSERT_NODE,
|
||||||
|
@ -277,6 +286,7 @@ function transformInterpolation(
|
||||||
type: IRNodeTypes.SET_TEXT,
|
type: IRNodeTypes.SET_TEXT,
|
||||||
loc: node.loc,
|
loc: node.loc,
|
||||||
element: id,
|
element: id,
|
||||||
|
value: expr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -300,20 +310,15 @@ function transformProp(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!node.exp) {
|
|
||||||
// TODO: Vue 3.4 supported shorthand syntax
|
|
||||||
// https://github.com/vuejs/core/pull/9451
|
|
||||||
return
|
|
||||||
} else if (node.exp.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)) {
|
|
||||||
// TODO: CompoundExpressionNode: :foo="count + 1"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.store = true
|
ctx.store = true
|
||||||
const expr = processExpression(ctx, node.exp.content)
|
const expr = processExpression(ctx, node.exp)
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'bind': {
|
case 'bind': {
|
||||||
if (!node.arg) {
|
if (expr === null) {
|
||||||
|
// TODO: Vue 3.4 supported shorthand syntax
|
||||||
|
// https://github.com/vuejs/core/pull/9451
|
||||||
|
return
|
||||||
|
} else if (!node.arg) {
|
||||||
// TODO support v-bind="{}"
|
// TODO support v-bind="{}"
|
||||||
return
|
return
|
||||||
} else if (
|
} else if (
|
||||||
|
@ -328,6 +333,7 @@ function transformProp(
|
||||||
loc: node.loc,
|
loc: node.loc,
|
||||||
element: ctx.getElementId(),
|
element: ctx.getElementId(),
|
||||||
name: node.arg.content,
|
name: node.arg.content,
|
||||||
|
value: expr,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -340,6 +346,10 @@ function transformProp(
|
||||||
) {
|
) {
|
||||||
// TODO support @[foo]="bar"
|
// TODO support @[foo]="bar"
|
||||||
return
|
return
|
||||||
|
} else if (expr === null) {
|
||||||
|
// TODO: support @foo
|
||||||
|
// https://github.com/vuejs/core/pull/9451
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.registerEffect(expr, {
|
ctx.registerEffect(expr, {
|
||||||
|
@ -347,30 +357,50 @@ function transformProp(
|
||||||
loc: node.loc,
|
loc: node.loc,
|
||||||
element: ctx.getElementId(),
|
element: ctx.getElementId(),
|
||||||
name: node.arg.content,
|
name: node.arg.content,
|
||||||
|
value: expr,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'html':
|
case 'html': {
|
||||||
ctx.registerEffect(expr, {
|
const value = expr || '""'
|
||||||
|
ctx.registerEffect(value, {
|
||||||
type: IRNodeTypes.SET_HTML,
|
type: IRNodeTypes.SET_HTML,
|
||||||
loc: node.loc,
|
loc: node.loc,
|
||||||
element: ctx.getElementId(),
|
element: ctx.getElementId(),
|
||||||
|
value,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'text':
|
}
|
||||||
ctx.registerEffect(expr, {
|
case 'text': {
|
||||||
|
const value = expr || '""'
|
||||||
|
ctx.registerEffect(value, {
|
||||||
type: IRNodeTypes.SET_TEXT,
|
type: IRNodeTypes.SET_TEXT,
|
||||||
loc: node.loc,
|
loc: node.loc,
|
||||||
element: ctx.getElementId(),
|
element: ctx.getElementId(),
|
||||||
|
value,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
case 'once': {
|
||||||
|
ctx.once = true
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: reuse packages/compiler-core/src/transforms/transformExpression.ts
|
// TODO: reuse packages/compiler-core/src/transforms/transformExpression.ts
|
||||||
function processExpression(ctx: TransformContext, expr: string) {
|
function processExpression(
|
||||||
if (ctx.options.bindingMetadata?.[expr] === 'setup-ref') {
|
ctx: TransformContext,
|
||||||
expr += '.value'
|
expr: ExpressionNode | undefined,
|
||||||
|
): string | null {
|
||||||
|
if (!expr) return null
|
||||||
|
if (expr.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)) {
|
||||||
|
// TODO
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
return expr
|
const { content } = expr
|
||||||
|
if (ctx.options.bindingMetadata?.[content] === 'setup-ref') {
|
||||||
|
return content + '.value'
|
||||||
|
}
|
||||||
|
return content
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
const count = ref(0)
|
const count = ref(1)
|
||||||
const double = computed(() => count.value * 2)
|
const double = computed(() => count.value * 2)
|
||||||
const html = computed(() => `<button>HTML! ${count.value}</button>`)
|
const html = computed(() => `<button>HTML! ${count.value}</button>`)
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ globalThis.html = html
|
||||||
</div>
|
</div>
|
||||||
<div v-html="html" />
|
<div v-html="html" />
|
||||||
<div v-text="html" />
|
<div v-text="html" />
|
||||||
|
<div v-once>once: {{ count }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue