-import { nextTick, ref } from '@vue/runtime-dom'
+import { nextTick, onActivated, ref } from '@vue/runtime-dom'
import { type VaporComponent, createComponent } from '../src/component'
import { defineVaporAsyncComponent } from '../src/apiDefineAsyncComponent'
import { makeRender } from './_utils'
import {
+ VaporKeepAlive,
createIf,
createTemplateRefSetter,
+ defineVaporComponent,
renderEffect,
template,
} from '@vue/runtime-vapor'
test.todo('suspense with error handling', async () => {})
- test.todo('with KeepAlive', async () => {})
+ test('with KeepAlive', async () => {
+ const spy = vi.fn()
+ let resolve: (comp: VaporComponent) => void
+
+ const Foo = defineVaporAsyncComponent(
+ () =>
+ new Promise(r => {
+ resolve = r as any
+ }),
+ )
+
+ const Bar = defineVaporAsyncComponent(() =>
+ Promise.resolve(
+ defineVaporComponent({
+ setup() {
+ return template('Bar')()
+ },
+ }),
+ ),
+ )
+
+ const toggle = ref(true)
+ const { html } = define({
+ setup() {
+ return createComponent(VaporKeepAlive, null, {
+ default: () =>
+ createIf(
+ () => toggle.value,
+ () => createComponent(Foo),
+ () => createComponent(Bar),
+ ),
+ })
+ },
+ }).render()
+ expect(html()).toBe('<!--async component--><!--if-->')
+
+ await nextTick()
+ resolve!(
+ defineVaporComponent({
+ setup() {
+ onActivated(() => {
+ spy()
+ })
+ return template('Foo')()
+ },
+ }),
+ )
+
+ await timeout()
+ expect(html()).toBe('Foo<!--async component--><!--if-->')
+ expect(spy).toBeCalledTimes(1)
- test.todo('with KeepAlive + include', async () => {})
+ toggle.value = false
+ await timeout()
+ expect(html()).toBe('Bar<!--async component--><!--if-->')
+ })
+
+ test('with KeepAlive + include', async () => {
+ const spy = vi.fn()
+ let resolve: (comp: VaporComponent) => void
+
+ const Foo = defineVaporAsyncComponent(
+ () =>
+ new Promise(r => {
+ resolve = r as any
+ }),
+ )
+
+ const { html } = define({
+ setup() {
+ return createComponent(
+ VaporKeepAlive,
+ { include: () => 'Foo' },
+ {
+ default: () => createComponent(Foo),
+ },
+ )
+ },
+ }).render()
+ expect(html()).toBe('<!--async component-->')
+
+ await nextTick()
+ resolve!(
+ defineVaporComponent({
+ name: 'Foo',
+ setup() {
+ onActivated(() => {
+ spy()
+ })
+ return template('Foo')()
+ },
+ }),
+ )
+
+ await timeout()
+ expect(html()).toBe('Foo<!--async component-->')
+ expect(spy).toBeCalledTimes(1)
+ })
})
createIf,
createTemplateRefSetter,
createVaporApp,
+ defineVaporAsyncComponent,
defineVaporComponent,
renderEffect,
setText,
} from '../../src'
const define = makeRender()
+const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
describe('VaporKeepAlive', () => {
let one: VaporComponent
})
})
- test.todo('should work with async component', async () => {})
+ test('should work with async component', async () => {
+ let resolve: (comp: VaporComponent) => void
+ const AsyncComp = defineVaporAsyncComponent(
+ () =>
+ new Promise(r => {
+ resolve = r as any
+ }),
+ )
+
+ const toggle = ref(true)
+ const instanceRef = ref<any>(null)
+ const { html } = define({
+ setup() {
+ const setRef = createTemplateRefSetter()
+ return createComponent(
+ VaporKeepAlive,
+ { include: () => 'Foo' },
+ {
+ default: () => {
+ return createIf(
+ () => toggle.value,
+ () => {
+ const n0 = createComponent(AsyncComp)
+ setRef(n0, instanceRef)
+ return n0
+ },
+ )
+ },
+ },
+ )
+ },
+ }).render()
+
+ expect(html()).toBe(`<!--async component--><!--if-->`)
+
+ resolve!(
+ defineVaporComponent({
+ name: 'Foo',
+ setup(_, { expose }) {
+ const count = ref(0)
+ expose({
+ inc: () => {
+ count.value++
+ },
+ })
+
+ const n0 = template(`<p> </p>`)() as any
+ const x0 = child(n0) as any
+ renderEffect(() => {
+ setText(x0, String(count.value))
+ })
+ return n0
+ },
+ }),
+ )
+
+ await timeout()
+ // resolved
+ expect(html()).toBe(`<p>0</p><!--async component--><!--if-->`)
+
+ // change state + toggle out
+ instanceRef.value.inc()
+ toggle.value = false
+ await nextTick()
+ expect(html()).toBe('<!--if-->')
+
+ // toggle in, state should be maintained
+ toggle.value = true
+ await nextTick()
+ expect(html()).toBe('<p>1</p><!--async component--><!--if-->')
+
+ toggle.value = false
+ await nextTick()
+ expect(html()).toBe('<!--if-->')
+ })
test('handle error in async onActivated', async () => {
const err = new Error('foo')
})
describe('vdom interop', () => {
- test('render vdom component', async () => {
+ test('should work', () => {
+ const VdomComp = {
+ setup() {
+ onBeforeMount(() => oneHooks.beforeMount())
+ onMounted(() => oneHooks.mounted())
+ onActivated(() => oneHooks.activated())
+ onDeactivated(() => oneHooks.deactivated())
+ onUnmounted(() => oneHooks.unmounted())
+ return () => h('div', null, 'hi')
+ },
+ }
+
+ const App = defineVaporComponent({
+ setup() {
+ return createComponent(VaporKeepAlive, null, {
+ default: () => {
+ return createComponent(VdomComp)
+ },
+ })
+ },
+ })
+
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+ const app = createVaporApp(App)
+ app.use(vaporInteropPlugin)
+ app.mount(container)
+
+ expect(container.innerHTML).toBe(`<div>hi</div>`)
+ assertHookCalls(oneHooks, [1, 1, 1, 0, 0])
+ })
+
+ test('with v-if', async () => {
const VdomComp = {
setup() {
const msg = ref('vdom')
createAsyncComponentContext,
currentInstance,
handleError,
+ isKeepAlive,
markAsyncBoundary,
performAsyncHydrate,
useAsyncComponentState,
import { invokeArrayFns } from '@vue/shared'
import { type TransitionOptions, insert, remove } from './block'
import { parentNode } from './dom/node'
+import type { KeepAliveInstance } from './components/KeepAlive'
import { setTransitionHooks } from './components/Transition'
/*@ __NO_SIDE_EFFECTS__ */
// already resolved
let resolvedComp = getResolvedComp()
if (resolvedComp) {
- frag!.update(() => createInnerComp(resolvedComp!, instance))
+ frag!.update(() => createInnerComp(resolvedComp!, instance, frag))
return frag
}
load()
.then(() => {
loaded.value = true
- // TODO parent is keep-alive, force update so the loaded component's
- // name is taken into account
})
.catch(err => {
onError(err)
appContext,
)
+ if (parent.parent && isKeepAlive(parent.parent)) {
+ // If there is a parent KeepAlive, let it handle the resolved async component
+ // This will process shapeFlag and cache the component
+ ;(parent.parent as KeepAliveInstance).cacheComponent(instance)
+ // cache the wrapper instance as well
+ ;(parent.parent as KeepAliveInstance).cacheComponent(parent)
+ }
+
// set transition hooks
if ($transition) setTransitionHooks(instance, $transition)
if (block.anchor) remove(block.anchor, parent)
if ((block as DynamicFragment).scope) {
;(block as DynamicFragment).scope!.stop()
+ const scopes = (block as DynamicFragment).keptAliveScopes
+ if (scopes) {
+ scopes.forEach(scope => scope.stop())
+ scopes.clear()
+ }
}
}
}
} from './dom/hydration'
import { _next, createElement } from './dom/node'
import { type TeleportFragment, isVaporTeleport } from './components/Teleport'
-import type { KeepAliveInstance } from './components/KeepAlive'
+import {
+ type KeepAliveInstance,
+ findParentKeepAlive,
+} from './components/KeepAlive'
import {
insertionAnchor,
insertionParent,
anchor?: Node | null | 0,
): void {
if (instance.shapeFlag! & ShapeFlags.COMPONENT_KEPT_ALIVE) {
- ;(instance.parent as KeepAliveInstance).activate(instance, parent, anchor)
+ findParentKeepAlive(instance)!.activate(instance, parent, anchor)
return
}
instance.parent.vapor &&
instance.shapeFlag! & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
) {
- ;(instance.parent as KeepAliveInstance).deactivate(instance)
+ findParentKeepAlive(instance)!.deactivate(instance)
return
}
import {
+ type GenericComponentInstance,
type KeepAliveProps,
+ type VNode,
currentInstance,
devtoolsComponentAdded,
getComponentName,
+ isAsyncWrapper,
+ isKeepAlive,
matches,
onBeforeUnmount,
onMounted,
import { defineVaporComponent } from '../apiDefineComponent'
import { ShapeFlags, invokeArrayFns, isArray } from '@vue/shared'
import { createElement } from '../dom/node'
-import { type VaporFragment, isFragment } from '../fragment'
+import {
+ type DynamicFragment,
+ type VaporFragment,
+ isFragment,
+} from '../fragment'
export interface KeepAliveInstance extends VaporComponentInstance {
activate: (
anchor?: Node | null | 0,
) => void
deactivate: (instance: VaporComponentInstance) => void
- process: (block: Block) => void
+ cacheComponent: (instance: VaporComponentInstance) => void
getCachedComponent: (
comp: VaporComponent,
) => VaporComponentInstance | VaporFragment | undefined
getStorageContainer: () => ParentNode
+ processFragment: (fragment: DynamicFragment) => void
+ cacheFragment: (fragment: DynamicFragment) => void
}
-type CacheKey = VaporComponent
+type CacheKey = VaporComponent | VNode['type']
type Cache = Map<CacheKey, VaporComponentInstance | VaporFragment>
type Keys = Set<CacheKey>
}
function shouldCache(instance: VaporComponentInstance) {
+ // For unresolved async wrappers, skip caching
+ // Wait for resolution and re-process in createInnerComp
+ if (isAsyncWrapper(instance) && !instance.type.__asyncResolved) {
+ return false
+ }
+
const { include, exclude } = props
- const name = getComponentName(instance.type)
+ const name = getComponentName(
+ isAsyncWrapper(instance)
+ ? instance.type.__asyncResolved!
+ : instance.type,
+ )
return !(
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
)
}
- function cacheBlock() {
+ function innerCacheBlock(
+ key: CacheKey,
+ instance: VaporComponentInstance | VaporFragment,
+ ) {
const { max } = props
- // TODO suspense
- const block = keepAliveInstance.block!
- const innerBlock = getInnerBlock(block)!
- if (!innerBlock || !shouldCache(innerBlock)) return
- const key = innerBlock.type
if (cache.has(key)) {
// make this key the freshest
keys.delete(key)
pruneCacheEntry(keys.values().next().value!)
}
}
- cache.set(
- key,
- (current =
- isFragment(block) && isFragment(block.nodes)
- ? // cache the fragment nodes for vdom interop
- block.nodes
- : innerBlock),
- )
+
+ cache.set(key, instance)
+ current = instance
+ }
+
+ function cacheBlock() {
+ // TODO suspense
+ const block = keepAliveInstance.block!
+ const innerBlock = getInnerBlock(block)!
+ if (!innerBlock || !shouldCache(innerBlock)) return
+
+ let toCache: VaporComponentInstance | VaporFragment
+ let key: CacheKey
+ let frag: VaporFragment | undefined
+ if (isFragment(block) && (frag = findInteropFragment(block))) {
+ // vdom component: cache the fragment
+ toCache = frag
+ key = frag.vnode!.type
+ } else {
+ // vapor component: cache the instance
+ toCache = innerBlock
+ key = innerBlock.type
+ }
+ innerCacheBlock(key, toCache)
}
onMounted(cacheBlock)
onUpdated(cacheBlock)
onBeforeUnmount(() => {
- cache.forEach(item => {
- const cached = getInnerComponent(item)!
- resetShapeFlag(cached)
- cache.delete(cached.type)
+ cache.forEach((cached, key) => {
+ const instance = getInstanceFromCache(cached)
+ if (!instance) return
+
+ resetCachedShapeFlag(cached)
+ cache.delete(key)
+
// current instance will be unmounted as part of keep-alive's unmount
if (current) {
- const innerComp = getInnerComponent(current)!
- if (innerComp.type === cached.type) {
- const instance = cached.vapor
- ? cached
- : // vdom interop
- (cached as any).component
+ const currentKey = isVaporComponent(current)
+ ? current.type
+ : current.vnode!.type
+ if (currentKey === key) {
+ // call deactivated hook
const da = instance.da
da && queuePostFlushCb(da)
return
}
}
- remove(item, storageContainer)
+
+ remove(cached, storageContainer)
})
})
keepAliveInstance.getStorageContainer = () => storageContainer
- keepAliveInstance.getCachedComponent = comp => cache.get(comp)
- const processShapeFlag = (keepAliveInstance.process = block => {
- const instance = getInnerComponent(block)
- if (!instance) return
+ keepAliveInstance.getCachedComponent = comp => {
+ return cache.get(comp)
+ }
+
+ keepAliveInstance.cacheComponent = (instance: VaporComponentInstance) => {
+ if (!shouldCache(instance)) return
+ instance.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+ innerCacheBlock(instance.type, instance)
+ }
+
+ keepAliveInstance.processFragment = (frag: DynamicFragment) => {
+ const innerBlock = getInnerBlock(frag.nodes)
+ if (!innerBlock) return
- if (cache.has(instance.type)) {
- instance.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE
+ const fragment = findInteropFragment(frag.nodes)
+ if (fragment) {
+ if (cache.has(fragment.vnode!.type)) {
+ fragment.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE
+ }
+ if (shouldCache(innerBlock)) {
+ fragment.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+ }
+ } else {
+ if (cache.has(innerBlock.type)) {
+ innerBlock.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE
+ }
+ if (shouldCache(innerBlock)) {
+ innerBlock.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+ }
}
+ }
+
+ keepAliveInstance.cacheFragment = (fragment: DynamicFragment) => {
+ const innerBlock = getInnerBlock(fragment.nodes)
+ if (!innerBlock || !shouldCache(innerBlock)) return
- if (shouldCache(instance)) {
- instance.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+ // Determine what to cache based on fragment type
+ let toCache: VaporComponentInstance | VaporFragment
+ let key: CacheKey
+
+ // find vdom interop fragment
+ const frag = findInteropFragment(fragment)
+ if (frag) {
+ frag.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+ toCache = frag
+ key = frag.vnode!.type
+ } else {
+ innerBlock.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+ toCache = innerBlock
+ key = innerBlock.type
}
- })
+
+ innerCacheBlock(key, toCache)
+ }
keepAliveInstance.activate = (instance, parentNode, anchor) => {
current = instance
deactivate(instance, storageContainer)
}
+ function resetCachedShapeFlag(
+ cached: VaporComponentInstance | VaporFragment,
+ ) {
+ if (isVaporComponent(cached)) {
+ resetShapeFlag(cached)
+ } else {
+ resetShapeFlag(cached.vnode)
+ }
+ }
+
let children = slots.default()
if (isArray(children) && children.length > 1) {
if (__DEV__) {
return children
}
- // `children` could be either a `VaporComponentInstance` or a `DynamicFragment`
- // (when using `v-if` or `<component is/>`). For `DynamicFragment` children,
- // the `shapeFlag` is processed in `DynamicFragment.update`. Here only need
- // to process the `VaporComponentInstance`
- if (isVaporComponent(children)) processShapeFlag(children)
+ // Process shapeFlag for vapor and vdom components
+ // DynamicFragment (v-if, <component is/>) is processed in DynamicFragment.update
+ if (isVaporComponent(children)) {
+ children.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+ } else if (isInteropFragment(children)) {
+ children.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+ }
function pruneCache(filter: (name: string) => boolean) {
- cache.forEach((instance, key) => {
- instance = getInnerComponent(instance)!
+ cache.forEach((cached, key) => {
+ const instance = getInstanceFromCache(cached)
+ if (!instance) return
const name = getComponentName(instance.type)
if (name && !filter(name)) {
pruneCacheEntry(key)
function pruneCacheEntry(key: CacheKey) {
const cached = cache.get(key)!
- resetShapeFlag(cached)
+
+ resetCachedShapeFlag(cached)
+
// don't unmount if the instance is the current one
if (cached !== current) {
remove(cached)
function getInnerBlock(block: Block): VaporComponentInstance | undefined {
if (isVaporComponent(block)) {
return block
- }
- if (isVdomInteropFragment(block)) {
+ } else if (isInteropFragment(block)) {
return block.vnode as any
- }
- if (isFragment(block)) {
+ } else if (isFragment(block)) {
return getInnerBlock(block.nodes)
}
}
-function getInnerComponent(block: Block): VaporComponentInstance | undefined {
- if (isVaporComponent(block)) {
+function isInteropFragment(block: Block): block is VaporFragment {
+ return !!(isFragment(block) && block.vnode)
+}
+
+function findInteropFragment(block: Block): VaporFragment | undefined {
+ if (isInteropFragment(block)) {
return block
- } else if (isVdomInteropFragment(block)) {
- // vdom interop
- return block.vnode as any
+ }
+ if (isFragment(block)) {
+ return findInteropFragment(block.nodes)
}
}
-function isVdomInteropFragment(block: Block): block is VaporFragment {
- return !!(isFragment(block) && block.insert)
+function getInstanceFromCache(
+ cached: VaporComponentInstance | VaporFragment,
+): GenericComponentInstance {
+ if (isVaporComponent(cached)) {
+ return cached
+ }
+ // vdom interop
+ return cached.vnode!.component as GenericComponentInstance
}
export function activate(
devtoolsComponentAdded(instance)
}
}
+
+export function findParentKeepAlive(
+ instance: VaporComponentInstance,
+): KeepAliveInstance | null {
+ let parent = instance as GenericComponentInstance | null
+ while (parent) {
+ if (isKeepAlive(parent)) {
+ return parent as KeepAliveInstance
+ }
+ parent = parent.parent
+ }
+ return null
+}
current?: BlockFn
fallback?: BlockFn
anchorLabel?: string
+ inKeepAlive?: boolean
+ keptAliveScopes?: Map<any, EffectScope>
constructor(anchorLabel?: string) {
super([])
const parent = isHydrating ? null : this.anchor.parentNode
const transition = this.$transition
const instance = currentInstance!
-
+ this.inKeepAlive = isKeepAlive(instance)
// teardown previous branch
if (this.scope) {
- if (isKeepAlive(instance)) {
- ;(instance as KeepAliveInstance).process(this.nodes)
+ if (this.inKeepAlive) {
+ ;(instance as KeepAliveInstance).processFragment(this)
+ if (!this.keptAliveScopes) this.keptAliveScopes = new Map()
+ this.keptAliveScopes.set(this.current, this.scope)
} else {
this.scope.stop()
}
parent: ParentNode | null,
) {
if (render) {
- this.scope = new EffectScope()
+ // For KeepAlive, try to reuse the keepAlive scope for this key
+ const scope =
+ this.inKeepAlive && this.keptAliveScopes
+ ? this.keptAliveScopes.get(this.current)
+ : undefined
+ if (scope) {
+ this.scope = scope
+ this.keptAliveScopes!.delete(this.current!)
+ this.scope.resume()
+ } else {
+ this.scope = new EffectScope()
+ }
+
this.nodes = this.scope.run(render) || []
- if (isKeepAlive(instance)) {
- ;(instance as KeepAliveInstance).process(this.nodes)
+ if (this.inKeepAlive) {
+ ;(instance as KeepAliveInstance).cacheFragment(this)
}
if (transition) {
this.$transition = applyTransitionHooks(this.nodes, transition)
import type { NodeRef } from './apiTemplateRef'
import { setTransitionHooks as setVaporTransitionHooks } from './components/Transition'
import {
- type KeepAliveInstance,
activate,
deactivate,
+ findParentKeepAlive,
} from './components/KeepAlive'
export const interopKey: unique symbol = Symbol(`interop`)
rawProps?: LooseRawProps | null,
rawSlots?: LooseRawSlots | null,
): VaporFragment {
+ const parentInstance = currentInstance as VaporComponentInstance
const frag = new VaporFragment([])
const vnode = (frag.vnode = createVNode(
component,
let rawRef: VNodeNormalizedRef | null = null
let isMounted = false
- const parentInstance = currentInstance as VaporComponentInstance
const unmount = (parentNode?: ParentNode, transition?: TransitionHooks) => {
// unset ref
if (rawRef) vdomSetRef(rawRef, null, null, vnode, true)
if (vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
vdomDeactivate(
vnode,
- (parentInstance as KeepAliveInstance).getStorageContainer(),
+ findParentKeepAlive(parentInstance)!.getStorageContainer(),
internals,
parentInstance as any,
null,
undefined,
false,
)
- return
- }
-
- const prev = currentInstance
- simpleSetCurrentInstance(parentInstance)
- if (!isMounted) {
- if (transition) setVNodeTransitionHooks(vnode, transition)
- internals.mt(
- vnode,
- parentNode,
- anchor,
- parentInstance as any,
- null,
- undefined,
- false,
- )
- // set ref
- if (rawRef) vdomSetRef(rawRef, null, null, vnode)
- onScopeDispose(unmount, true)
- isMounted = true
} else {
- // move
- internals.m(
- vnode,
- parentNode,
- anchor,
- MoveType.REORDER,
- parentInstance as any,
- )
+ const prev = currentInstance
+ simpleSetCurrentInstance(parentInstance)
+ if (!isMounted) {
+ if (transition) setVNodeTransitionHooks(vnode, transition)
+ internals.mt(
+ vnode,
+ parentNode,
+ anchor,
+ parentInstance as any,
+ null,
+ undefined,
+ false,
+ )
+ // set ref
+ if (rawRef) vdomSetRef(rawRef, null, null, vnode)
+ onScopeDispose(unmount, true)
+ isMounted = true
+ } else {
+ // move
+ internals.m(
+ vnode,
+ parentNode,
+ anchor,
+ MoveType.REORDER,
+ parentInstance as any,
+ )
+ }
+ simpleSetCurrentInstance(prev)
}
frag.nodes = vnode.el as any
- simpleSetCurrentInstance(prev)
}
frag.remove = unmount