From: 三咲智子 Kevin Deng Date: Fri, 24 Nov 2023 07:25:34 +0000 (+0800) Subject: feat: once X-Git-Tag: v3.6.0-alpha.1~16^2~826 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=7ddf69e6e933b7959bab74e91e42f28609c061ac;p=thirdparty%2Fvuejs%2Fcore.git feat: once --- diff --git a/README.md b/README.md index f1100c7033..0fe3f8d016 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/packages/compiler-vapor/__tests__/__snapshots__/basic.test.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/basic.test.ts.snap index 7a2ed91f73..d0365c84f2 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/basic.test.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/basic.test.ts.snap @@ -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(\`

Counter

Count:

Double:

\`) +const t0 = template(\`

Counter

Count:

Double:

once:

\`) import { ref, computed } from 'vue' const html = 'HTML' @@ -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) }) diff --git a/packages/compiler-vapor/__tests__/fixtures/counter.vue b/packages/compiler-vapor/__tests__/fixtures/counter.vue index 14d9d669e9..17d24cb353 100644 --- a/packages/compiler-vapor/__tests__/fixtures/counter.vue +++ b/packages/compiler-vapor/__tests__/fixtures/counter.vue @@ -16,4 +16,5 @@ const html = 'HTML'
+

once: {{ count }}

diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index 22262a8dd1..bef9705bde 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -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) { diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 00b7516f38..7e0e8da050 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -21,7 +21,8 @@ export interface RootIRNode extends IRNode { type: IRNodeTypes.ROOT template: Array children: DynamicChildren - effect: Record + // TODO multi-expression effect + effect: Record operation: OperationNode[] helpers: Set vaporHelpers: Set @@ -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 diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index a1599640b1..d529b83045 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -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 { 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( 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 '' + } + const { content } = expr + if (ctx.options.bindingMetadata?.[content] === 'setup-ref') { + return content + '.value' } - return expr + return content } diff --git a/playground/src/App.vue b/playground/src/App.vue index 893205fc99..1abab002cf 100644 --- a/playground/src/App.vue +++ b/playground/src/App.vue @@ -1,7 +1,7 @@