feat(runtime-vapor): add support for v-once

This commit is contained in:
daiwei 2025-06-10 16:01:54 +08:00
parent 1ef6e6edb7
commit 9087cdf3cb
5 changed files with 98 additions and 7 deletions

View File

@ -5,6 +5,7 @@ import {
onUpdated, onUpdated,
provide, provide,
ref, ref,
useAttrs,
watch, watch,
watchEffect, watchEffect,
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
@ -12,6 +13,7 @@ import {
createComponent, createComponent,
createIf, createIf,
createTextNode, createTextNode,
defineVaporComponent,
renderEffect, renderEffect,
template, template,
} from '../src' } from '../src'
@ -288,6 +290,66 @@ describe('component', () => {
expect(i.scope.effects.length).toBe(0) 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', () => { test('should mount component only with template in production mode', () => {
__DEV__ = false __DEV__ = false
const { component: Child } = define({ const { component: Child } = define({

View File

@ -41,6 +41,7 @@ const mountApp: AppMountFn<ParentNode> = (app, container) => {
app._props as RawProps, app._props as RawProps,
null, null,
false, false,
false,
app._context, app._context,
) )
mountComponent(instance, container) mountComponent(instance, container)
@ -61,6 +62,7 @@ const hydrateApp: AppMountFn<ParentNode> = (app, container) => {
app._props as RawProps, app._props as RawProps,
null, null,
false, false,
false,
app._context, app._context,
) )
mountComponent(instance, container) mountComponent(instance, container)

View File

@ -10,6 +10,7 @@ export function createDynamicComponent(
rawProps?: RawProps | null, rawProps?: RawProps | null,
rawSlots?: RawSlots | null, rawSlots?: RawSlots | null,
isSingleRoot?: boolean, isSingleRoot?: boolean,
once?: boolean,
): VaporFragment { ): VaporFragment {
const frag = __DEV__ const frag = __DEV__
? new DynamicFragment('dynamic-component') ? new DynamicFragment('dynamic-component')
@ -23,6 +24,7 @@ export function createDynamicComponent(
rawProps, rawProps,
rawSlots, rawSlots,
isSingleRoot, isSingleRoot,
once,
), ),
value, value,
) )

View File

@ -134,6 +134,7 @@ export function createComponent(
rawProps?: LooseRawProps | null, rawProps?: LooseRawProps | null,
rawSlots?: LooseRawSlots | null, rawSlots?: LooseRawSlots | null,
isSingleRoot?: boolean, isSingleRoot?: boolean,
once?: boolean,
appContext: GenericAppContext = (currentInstance && appContext: GenericAppContext = (currentInstance &&
currentInstance.appContext) || currentInstance.appContext) ||
emptyContext, emptyContext,
@ -180,6 +181,7 @@ export function createComponent(
rawProps as RawProps, rawProps as RawProps,
rawSlots as RawSlots, rawSlots as RawSlots,
appContext, appContext,
once,
) )
if (__DEV__) { if (__DEV__) {
@ -380,6 +382,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
rawProps?: RawProps | null, rawProps?: RawProps | null,
rawSlots?: RawSlots | null, rawSlots?: RawSlots | null,
appContext?: GenericAppContext, appContext?: GenericAppContext,
once?: boolean,
) { ) {
this.vapor = true this.vapor = true
this.uid = nextUid() this.uid = nextUid()
@ -420,7 +423,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
this.rawProps = rawProps || EMPTY_OBJ this.rawProps = rawProps || EMPTY_OBJ
this.hasFallthrough = hasFallthroughAttrs(comp, rawProps) this.hasFallthrough = hasFallthroughAttrs(comp, rawProps)
if (rawProps || comp.props) { if (rawProps || comp.props) {
const [propsHandlers, attrsHandlers] = getPropsProxyHandlers(comp) const [propsHandlers, attrsHandlers] = getPropsProxyHandlers(comp, once)
this.attrs = new Proxy(this, attrsHandlers) this.attrs = new Proxy(this, attrsHandlers)
this.props = comp.props this.props = comp.props
? new Proxy(this, propsHandlers!) ? new Proxy(this, propsHandlers!)
@ -465,9 +468,10 @@ export function createComponentWithFallback(
rawProps?: LooseRawProps | null, rawProps?: LooseRawProps | null,
rawSlots?: LooseRawSlots | null, rawSlots?: LooseRawSlots | null,
isSingleRoot?: boolean, isSingleRoot?: boolean,
once?: boolean,
): HTMLElement | VaporComponentInstance { ): HTMLElement | VaporComponentInstance {
if (!isString(comp)) { if (!isString(comp)) {
return createComponent(comp, rawProps, rawSlots, isSingleRoot) return createComponent(comp, rawProps, rawSlots, isSingleRoot, once)
} }
const el = document.createElement(comp) const el = document.createElement(comp)

View File

@ -23,6 +23,7 @@ import {
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
import { normalizeEmitsOptions } from './componentEmits' import { normalizeEmitsOptions } from './componentEmits'
import { renderEffect } from './renderEffect' import { renderEffect } from './renderEffect'
import { pauseTracking, resetTracking } from '@vue/reactivity'
export type RawProps = Record<string, () => unknown> & { export type RawProps = Record<string, () => unknown> & {
// generated by compiler for :[key]="x" or v-bind="x" // generated by compiler for :[key]="x" or v-bind="x"
@ -42,6 +43,7 @@ export function resolveSource(
export function getPropsProxyHandlers( export function getPropsProxyHandlers(
comp: VaporComponent, comp: VaporComponent,
once?: boolean,
): [ ): [
ProxyHandler<VaporComponentInstance> | null, ProxyHandler<VaporComponentInstance> | null,
ProxyHandler<VaporComponentInstance>, 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 const propsHandlers = propsOptions
? ({ ? ({
get: (target, key) => getProp(target, key), get: (target, key) => getPropValue(target, key),
has: (_, key) => isProp(key), has: (_, key) => isProp(key),
ownKeys: () => Object.keys(propsOptions), ownKeys: () => Object.keys(propsOptions),
getOwnPropertyDescriptor(target, key) { getOwnPropertyDescriptor(target, key) {
@ -117,7 +128,7 @@ export function getPropsProxyHandlers(
return { return {
configurable: true, configurable: true,
enumerable: 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 = { 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), has: (target, key: string) => hasAttr(target.rawProps, key),
ownKeys: target => getKeysFromRawProps(target.rawProps).filter(isAttr), ownKeys: target => getKeysFromRawProps(target.rawProps).filter(isAttr),
getOwnPropertyDescriptor(target, key: string) { getOwnPropertyDescriptor(target, key: string) {
@ -154,7 +174,7 @@ export function getPropsProxyHandlers(
return { return {
configurable: true, configurable: true,
enumerable: 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) { if (dynamicSources) {
let i = dynamicSources.length let i = dynamicSources.length
while (i--) { while (i--) {
if (hasOwn(resolveSource(dynamicSources[i]), key)) { const source = resolveSource(dynamicSources[i])
if (source && hasOwn(source, key)) {
return true return true
} }
} }