]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(runtime-vapor): add support for v-once
authordaiwei <daiwei521@126.com>
Tue, 10 Jun 2025 08:01:54 +0000 (16:01 +0800)
committerdaiwei <daiwei521@126.com>
Tue, 10 Jun 2025 08:01:54 +0000 (16:01 +0800)
packages/runtime-vapor/__tests__/component.spec.ts
packages/runtime-vapor/src/apiCreateApp.ts
packages/runtime-vapor/src/apiCreateDynamicComponent.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/componentProps.ts

index 5fdff8eafe4453b437af4b07e561191639c1d045..871df18077583069225ace9259adb77f52aa3426 100644 (file)
@@ -5,6 +5,7 @@ import {
   onUpdated,
   provide,
   ref,
+  useAttrs,
   watch,
   watchEffect,
 } from '@vue/runtime-dom'
@@ -12,6 +13,7 @@ import {
   createComponent,
   createIf,
   createTextNode,
+  defineVaporComponent,
   renderEffect,
   template,
 } from '../src'
@@ -288,6 +290,66 @@ describe('component', () => {
     expect(i.scope.effects.length).toBe(0)
   })
 
+  it('work with v-once + props', () => {
+    const Child = defineVaporComponent({
+      props: {
+        count: Number,
+      },
+      setup(props) {
+        const n0 = template(' ')() as any
+        renderEffect(() => setText(n0, props.count))
+        return n0
+      },
+    })
+
+    const count = ref(0)
+    const { html } = define({
+      setup() {
+        return createComponent(
+          Child,
+          { count: () => count.value },
+          null,
+          true,
+          true, // v-once
+        )
+      },
+    }).render()
+
+    expect(html()).toBe('0')
+
+    count.value++
+    expect(html()).toBe('0')
+  })
+
+  it('work with v-once + attrs', () => {
+    const Child = defineVaporComponent({
+      setup() {
+        const attrs = useAttrs()
+        const n0 = template(' ')() as any
+        renderEffect(() => setText(n0, attrs.count as string))
+        return n0
+      },
+    })
+
+    const count = ref(0)
+    const { html } = define({
+      setup() {
+        return createComponent(
+          Child,
+          { count: () => count.value },
+          null,
+          true,
+          true, // v-once
+        )
+      },
+    }).render()
+
+    expect(html()).toBe('0')
+
+    count.value++
+    expect(html()).toBe('0')
+  })
+
   test('should mount component only with template in production mode', () => {
     __DEV__ = false
     const { component: Child } = define({
index 834437ee350b0cd06d3b617ab59afa4bf5d1df8e..3da4e2ec7dca4e72fbccb774e7e431700ba35344 100644 (file)
@@ -41,6 +41,7 @@ const mountApp: AppMountFn<ParentNode> = (app, container) => {
     app._props as RawProps,
     null,
     false,
+    false,
     app._context,
   )
   mountComponent(instance, container)
@@ -61,6 +62,7 @@ const hydrateApp: AppMountFn<ParentNode> = (app, container) => {
       app._props as RawProps,
       null,
       false,
+      false,
       app._context,
     )
     mountComponent(instance, container)
index 2126611d7182e1cb548d4179e09dd536c8f3c920..a8f55bab6344e61b9f1dd4d25533702d0bd1a04a 100644 (file)
@@ -10,6 +10,7 @@ export function createDynamicComponent(
   rawProps?: RawProps | null,
   rawSlots?: RawSlots | null,
   isSingleRoot?: boolean,
+  once?: boolean,
 ): VaporFragment {
   const frag = __DEV__
     ? new DynamicFragment('dynamic-component')
@@ -23,6 +24,7 @@ export function createDynamicComponent(
           rawProps,
           rawSlots,
           isSingleRoot,
+          once,
         ),
       value,
     )
index 548babebf8beef2115e31356d50a989e2e1a0112..a2b2342d93db75c95aaa47fe456c15d5432cb606 100644 (file)
@@ -134,6 +134,7 @@ export function createComponent(
   rawProps?: LooseRawProps | null,
   rawSlots?: LooseRawSlots | null,
   isSingleRoot?: boolean,
+  once?: boolean,
   appContext: GenericAppContext = (currentInstance &&
     currentInstance.appContext) ||
     emptyContext,
@@ -180,6 +181,7 @@ export function createComponent(
     rawProps as RawProps,
     rawSlots as RawSlots,
     appContext,
+    once,
   )
 
   if (__DEV__) {
@@ -380,6 +382,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
     rawProps?: RawProps | null,
     rawSlots?: RawSlots | null,
     appContext?: GenericAppContext,
+    once?: boolean,
   ) {
     this.vapor = true
     this.uid = nextUid()
@@ -420,7 +423,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
     this.rawProps = rawProps || EMPTY_OBJ
     this.hasFallthrough = hasFallthroughAttrs(comp, rawProps)
     if (rawProps || comp.props) {
-      const [propsHandlers, attrsHandlers] = getPropsProxyHandlers(comp)
+      const [propsHandlers, attrsHandlers] = getPropsProxyHandlers(comp, once)
       this.attrs = new Proxy(this, attrsHandlers)
       this.props = comp.props
         ? new Proxy(this, propsHandlers!)
@@ -465,9 +468,10 @@ export function createComponentWithFallback(
   rawProps?: LooseRawProps | null,
   rawSlots?: LooseRawSlots | null,
   isSingleRoot?: boolean,
+  once?: boolean,
 ): HTMLElement | VaporComponentInstance {
   if (!isString(comp)) {
-    return createComponent(comp, rawProps, rawSlots, isSingleRoot)
+    return createComponent(comp, rawProps, rawSlots, isSingleRoot, once)
   }
 
   const el = document.createElement(comp)
index a5e9daad229ca25c684614de03d195ebc2aee985..dbc1386e3969afc7da83b7c7bda1647a88b26d0f 100644 (file)
@@ -23,6 +23,7 @@ import {
 } from '@vue/runtime-dom'
 import { normalizeEmitsOptions } from './componentEmits'
 import { renderEffect } from './renderEffect'
+import { pauseTracking, resetTracking } from '@vue/reactivity'
 
 export type RawProps = Record<string, () => unknown> & {
   // generated by compiler for :[key]="x" or v-bind="x"
@@ -42,6 +43,7 @@ export function resolveSource(
 
 export function getPropsProxyHandlers(
   comp: VaporComponent,
+  once?: boolean,
 ): [
   ProxyHandler<VaporComponentInstance> | null,
   ProxyHandler<VaporComponentInstance>,
@@ -107,9 +109,18 @@ export function getPropsProxyHandlers(
     )
   }
 
+  const getPropValue = once
+    ? (...args: Parameters<typeof getProp>) => {
+        pauseTracking()
+        const value = getProp(...args)
+        resetTracking()
+        return value
+      }
+    : getProp
+
   const propsHandlers = propsOptions
     ? ({
-        get: (target, key) => getProp(target, key),
+        get: (target, key) => getPropValue(target, key),
         has: (_, key) => isProp(key),
         ownKeys: () => Object.keys(propsOptions),
         getOwnPropertyDescriptor(target, key) {
@@ -117,7 +128,7 @@ export function getPropsProxyHandlers(
             return {
               configurable: true,
               enumerable: true,
-              get: () => getProp(target, key),
+              get: () => getPropValue(target, key),
             }
           }
         },
@@ -145,8 +156,17 @@ export function getPropsProxyHandlers(
     }
   }
 
+  const getAttrValue = once
+    ? (...args: Parameters<typeof getAttr>) => {
+        pauseTracking()
+        const value = getAttr(...args)
+        resetTracking()
+        return value
+      }
+    : getAttr
+
   const attrsHandlers = {
-    get: (target, key: string) => getAttr(target.rawProps, key),
+    get: (target, key: string) => getAttrValue(target.rawProps, key),
     has: (target, key: string) => hasAttr(target.rawProps, key),
     ownKeys: target => getKeysFromRawProps(target.rawProps).filter(isAttr),
     getOwnPropertyDescriptor(target, key: string) {
@@ -154,7 +174,7 @@ export function getPropsProxyHandlers(
         return {
           configurable: true,
           enumerable: true,
-          get: () => getAttr(target.rawProps, key),
+          get: () => getAttrValue(target.rawProps, key),
         }
       }
     },
@@ -210,7 +230,8 @@ export function hasAttrFromRawProps(rawProps: RawProps, key: string): boolean {
   if (dynamicSources) {
     let i = dynamicSources.length
     while (i--) {
-      if (hasOwn(resolveSource(dynamicSources[i]), key)) {
+      const source = resolveSource(dynamicSources[i])
+      if (source && hasOwn(source, key)) {
         return true
       }
     }