feat: once

This commit is contained in:
三咲智子 Kevin Deng 2023-11-24 15:25:34 +08:00
parent 0b07affe0b
commit 7ddf69e6e9
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
7 changed files with 144 additions and 88 deletions

View File

@ -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`

View File

@ -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)
})

View File

@ -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>

View File

@ -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) {

View File

@ -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

View File

@ -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
}

View File

@ -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>