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
}
}