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