]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: once
author三咲智子 Kevin Deng <sxzz@sxzz.moe>
Fri, 24 Nov 2023 07:25:34 +0000 (15:25 +0800)
committer三咲智子 Kevin Deng <sxzz@sxzz.moe>
Fri, 24 Nov 2023 07:25:34 +0000 (15:25 +0800)
README.md
packages/compiler-vapor/__tests__/__snapshots__/basic.test.ts.snap
packages/compiler-vapor/__tests__/fixtures/counter.vue
packages/compiler-vapor/src/generate.ts
packages/compiler-vapor/src/ir.ts
packages/compiler-vapor/src/transform.ts
playground/src/App.vue

index f1100c7033180c6e77a16ee5a3e5e2240933fcbd..0fe3f8d016cddc773112d1735efca220e25713f5 100644 (file)
--- 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`
index 7a2ed91f7300f7992ad4acf5693f56480a47b1bd..d0365c84f23e889403c3f6cd79e3b9e3434a9a8f 100644 (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)
 })
index 14d9d669e953528c7a5870ab13049813aaee55a6..17d24cb353094a6cde97fb952268c6d35abfb134 100644 (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>
index 22262a8dd16931c4f12fdce875e970495413dcf1..bef9705bdea30d227349b89a4b55d89eb998c9d6 100644 (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) {
index 00b7516f382643efd3ebca2a2dce6e95be19f0b7..7e0e8da0502e7e3936a0c0676ae21307de44e74c 100644 (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
index a1599640b1aeb744aa0e3548d28fce3ce0e77483..d529b83045b11281b98501069e1e556d31824132 100644 (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 ''
+  }
+  const { content } = expr
+  if (ctx.options.bindingMetadata?.[content] === 'setup-ref') {
+    return content + '.value'
   }
-  return expr
+  return content
 }
index 893205fc99dd5ecacd68f480d9dfdade11c60e2c..1abab002cfbab4a0baaeb4b2498876f2c1af5425 100644 (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>