diff --git a/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts b/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts index 2f6cd7b3f39..448bd2c315b 100644 --- a/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts +++ b/packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts @@ -1,4 +1,4 @@ -import { shallowRef } from '@vue/reactivity' +import { ref, shallowRef } from '@vue/reactivity' import { nextTick } from '@vue/runtime-dom' import { createDynamicComponent } from '../src' import { makeRender } from './_utils' @@ -54,4 +54,42 @@ describe('api: createDynamicComponent', () => { await nextTick() 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('') + }) }) diff --git a/packages/runtime-vapor/__tests__/component.spec.ts b/packages/runtime-vapor/__tests__/component.spec.ts index 5fdff8eafe4..871df180775 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 834437ee350..3da4e2ec7dc 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 2126611d718..a1b1e3e98f3 100644 --- a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts +++ b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts @@ -10,11 +10,13 @@ export function createDynamicComponent( rawProps?: RawProps | null, rawSlots?: RawSlots | null, isSingleRoot?: boolean, + once?: boolean, ): VaporFragment { const frag = __DEV__ ? new DynamicFragment('dynamic-component') : new DynamicFragment() - renderEffect(() => { + + const renderFn = () => { const value = getter() frag.update( () => @@ -23,9 +25,14 @@ export function createDynamicComponent( rawProps, rawSlots, isSingleRoot, + once, ), value, ) - }) + } + + if (once) renderFn() + else renderEffect(renderFn) + return frag } diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 548babebf8b..27f14f71228 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) @@ -475,9 +479,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 a5e9daad229..dbc1386e396 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 } }