From 9087cdf3cb271d547cfbc5de3526aaecc03f8879 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 10 Jun 2025 16:01:54 +0800 Subject: [PATCH] feat(runtime-vapor): add support for v-once --- .../runtime-vapor/__tests__/component.spec.ts | 62 +++++++++++++++++++ packages/runtime-vapor/src/apiCreateApp.ts | 2 + .../src/apiCreateDynamicComponent.ts | 2 + packages/runtime-vapor/src/component.ts | 8 ++- packages/runtime-vapor/src/componentProps.ts | 31 ++++++++-- 5 files changed, 98 insertions(+), 7 deletions(-) diff --git a/packages/runtime-vapor/__tests__/component.spec.ts b/packages/runtime-vapor/__tests__/component.spec.ts index 5fdff8eafe..871df18077 100644 --- a/packages/runtime-vapor/__tests__/component.spec.ts +++ b/packages/runtime-vapor/__tests__/component.spec.ts @@ -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({ diff --git a/packages/runtime-vapor/src/apiCreateApp.ts b/packages/runtime-vapor/src/apiCreateApp.ts index 834437ee35..3da4e2ec7d 100644 --- a/packages/runtime-vapor/src/apiCreateApp.ts +++ b/packages/runtime-vapor/src/apiCreateApp.ts @@ -41,6 +41,7 @@ const mountApp: AppMountFn = (app, container) => { app._props as RawProps, null, false, + false, app._context, ) mountComponent(instance, container) @@ -61,6 +62,7 @@ const hydrateApp: AppMountFn = (app, container) => { app._props as RawProps, null, false, + false, app._context, ) mountComponent(instance, container) diff --git a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts index 2126611d71..a8f55bab63 100644 --- a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts +++ b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts @@ -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, ) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 548babebf8..a2b2342d93 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -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) diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index a5e9daad22..dbc1386e39 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -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 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 | null, ProxyHandler, @@ -107,9 +109,18 @@ export function getPropsProxyHandlers( ) } + const getPropValue = once + ? (...args: Parameters) => { + 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) => { + 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 } } -- 2.47.2