diff --git a/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts b/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts index e912af2851a..89514e17701 100644 --- a/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts +++ b/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts @@ -63,6 +63,44 @@ describe('api: createDynamicComponent', () => { expect(html()).toBe('') }) + test('with v-once', async () => { + const val = shallowRef(A) + + const { html } = define({ + setup() { + return createDynamicComponent(() => val.value, null, null, true, true) + }, + }).render() + + expect(html()).toBe('AAA') + + val.value = B + await nextTick() + expect(html()).toBe('AAA') // still AAA + }) + + test('fallback with v-once', async () => { + const val = shallowRef('button') + const id = ref(0) + const { html } = define({ + setup() { + return createDynamicComponent( + () => val.value, + { id: () => id.value }, + null, + true, + true, + ) + }, + }).render() + + expect(html()).toBe('') + + id.value++ + await nextTick() + expect(html()).toBe('') + }) + test('render fallback with insertionState', async () => { const { html, mount } = define({ setup() { diff --git a/packages/runtime-vapor/__tests__/component.spec.ts b/packages/runtime-vapor/__tests__/component.spec.ts index b96a932a2f3..ce901e19931 100644 --- a/packages/runtime-vapor/__tests__/component.spec.ts +++ b/packages/runtime-vapor/__tests__/component.spec.ts @@ -8,6 +8,7 @@ import { onUpdated, provide, ref, + useAttrs, watch, watchEffect, } from '@vue/runtime-dom' @@ -15,6 +16,7 @@ import { createComponent, createIf, createTextNode, + defineVaporComponent, renderEffect, setInsertionState, template, @@ -315,6 +317,66 @@ describe('component', () => { expect(getEffectsCount(i.scope)).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 ee4c00c880d..f6a9b139a33 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 945e0f38d87..9e51e81d976 100644 --- a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts +++ b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts @@ -16,6 +16,7 @@ export function createDynamicComponent( rawProps?: RawProps | null, rawSlots?: RawSlots | null, isSingleRoot?: boolean, + once?: boolean, ): VaporFragment { const _insertionParent = insertionParent const _insertionAnchor = insertionAnchor @@ -29,7 +30,7 @@ export function createDynamicComponent( ? new DynamicFragment('dynamic-component') : new DynamicFragment() - renderEffect(() => { + const renderFn = () => { const value = getter() frag.update( () => @@ -38,10 +39,14 @@ export function createDynamicComponent( rawProps, rawSlots, isSingleRoot, + once, ), value, ) - }) + } + + if (once) renderFn() + else renderEffect(renderFn) if (!isHydrating && _insertionParent) { insert(frag, _insertionParent, _insertionAnchor) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index da57882c49d..83656aa55bf 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -137,6 +137,7 @@ export function createComponent( rawProps?: LooseRawProps | null, rawSlots?: LooseRawSlots | null, isSingleRoot?: boolean, + once?: boolean, appContext: GenericAppContext = (currentInstance && currentInstance.appContext) || emptyContext, @@ -185,6 +186,7 @@ export function createComponent( rawProps as RawProps, rawSlots as RawSlots, appContext, + once, ) // HMR @@ -401,6 +403,7 @@ export class VaporComponentInstance implements GenericComponentInstance { rawProps?: RawProps | null, rawSlots?: RawSlots | null, appContext?: GenericAppContext, + once?: boolean, ) { this.vapor = true this.uid = nextUid() @@ -441,7 +444,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!) @@ -486,9 +489,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 _insertionParent = insertionParent @@ -504,9 +508,10 @@ export function createComponentWithFallback( ;(el as any).$root = isSingleRoot if (rawProps) { - renderEffect(() => { + const setFn = () => setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)]) - }) + if (once) setFn() + else renderEffect(setFn) } if (rawSlots) { diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index 55eadb98046..6832bd9103c 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -23,6 +23,7 @@ import { import { ReactiveFlags } from '@vue/reactivity' import { normalizeEmitsOptions } from './componentEmits' import { renderEffect } from './renderEffect' +import { pauseTracking, resetTracking } from '@vue/reactivity' import type { interopKey } from './vdomInterop' export type RawProps = Record unknown> & { @@ -43,6 +44,7 @@ export function resolveSource( export function getPropsProxyHandlers( comp: VaporComponent, + once?: boolean, ): [ ProxyHandler | null, ProxyHandler, @@ -111,9 +113,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) { @@ -121,7 +132,7 @@ export function getPropsProxyHandlers( return { configurable: true, enumerable: true, - get: () => getProp(target, key), + get: () => getPropValue(target, key), } } }, @@ -149,8 +160,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) { @@ -158,7 +178,7 @@ export function getPropsProxyHandlers( return { configurable: true, enumerable: true, - get: () => getAttr(target.rawProps, key), + get: () => getAttrValue(target.rawProps, key), } } },