import {
+ type AsyncComponentInternalOptions,
+ type GenericComponent,
type GenericComponentInstance,
type KeepAliveProps,
type VNode,
import {
type DynamicFragment,
type VaporFragment,
+ isDynamicFragment,
isFragment,
} from '../fragment'
+import type { EffectScope } from '@vue/reactivity'
export interface KeepAliveInstance extends VaporComponentInstance {
activate: (
anchor?: Node | null | 0,
) => void
deactivate: (instance: VaporComponentInstance) => void
- cacheComponent: (instance: VaporComponentInstance) => void
getCachedComponent: (
comp: VaporComponent,
) => VaporComponentInstance | VaporFragment | undefined
getStorageContainer: () => ParentNode
- processFragment: (fragment: DynamicFragment) => void
- cacheFragment: (fragment: DynamicFragment) => void
}
type CacheKey = VaporComponent | VNode['type']
const cache: Cache = new Map()
const keys: Keys = new Set()
const storageContainer = createElement('div')
+ const keptAliveScopes = new Map<any, EffectScope>()
let current: VaporComponentInstance | VaporFragment | undefined
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
;(keepAliveInstance as any).__v_cache = cache
}
- 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
- }
+ keepAliveInstance.getStorageContainer = () => storageContainer
- const { include, exclude } = props
- const name = getComponentName(
- isAsyncWrapper(instance)
- ? instance.type.__asyncResolved!
- : instance.type,
- )
- return !(
- (include && (!name || !matches(include, name))) ||
- (exclude && name && matches(exclude, name))
- )
+ keepAliveInstance.getCachedComponent = comp => cache.get(comp)
+
+ keepAliveInstance.activate = (instance, parentNode, anchor) => {
+ current = instance
+ activate(instance, parentNode, anchor)
+ }
+
+ keepAliveInstance.deactivate = instance => {
+ current = undefined
+ deactivate(instance, storageContainer)
}
- function innerCacheBlock(
+ const innerCacheBlock = (
key: CacheKey,
instance: VaporComponentInstance | VaporFragment,
- ) {
+ ) => {
const { max } = props
if (cache.has(key)) {
current = instance
}
- function cacheBlock() {
+ const 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((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 currentKey = isVaporComponent(current)
- ? current.type
- : current.vnode!.type
- if (currentKey === key) {
- // call deactivated hook
- const da = instance.da
- da && queuePostFlushCb(da)
- return
- }
- }
-
- remove(cached, storageContainer)
- })
- })
-
- keepAliveInstance.getStorageContainer = () => storageContainer
-
- 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)
+ const [innerBlock, interop] = getInnerBlock(block)!
+ if (!innerBlock || !shouldCache(innerBlock, props, interop)) return
+ innerCacheBlock(
+ interop ? innerBlock.vnode!.type : innerBlock.type,
+ innerBlock,
+ )
}
- keepAliveInstance.processFragment = (frag: DynamicFragment) => {
- const innerBlock = getInnerBlock(frag.nodes)
- if (!innerBlock) return
+ const processFragment = (frag: DynamicFragment) => {
+ const [innerBlock, interop] = getInnerBlock(frag.nodes)
+ if (!innerBlock && !shouldCache(innerBlock!, props, interop)) return
- const fragment = findInteropFragment(frag.nodes)
- if (fragment) {
- if (cache.has(fragment.vnode!.type)) {
- fragment.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE
+ if (interop) {
+ if (cache.has(innerBlock.vnode!.type)) {
+ innerBlock.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE
}
- if (shouldCache(innerBlock)) {
- fragment.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+ if (shouldCache(innerBlock!, props, true)) {
+ innerBlock.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
}
} else {
- if (cache.has(innerBlock.type)) {
- innerBlock.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE
+ if (cache.has(innerBlock!.type)) {
+ innerBlock!.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE
}
- if (shouldCache(innerBlock)) {
- innerBlock.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+ if (shouldCache(innerBlock!, props)) {
+ innerBlock!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
}
}
}
- keepAliveInstance.cacheFragment = (fragment: DynamicFragment) => {
- const innerBlock = getInnerBlock(fragment.nodes)
- if (!innerBlock || !shouldCache(innerBlock)) return
+ const cacheFragment = (fragment: DynamicFragment) => {
+ const [innerBlock, interop] = getInnerBlock(fragment.nodes)
+ if (!innerBlock || !shouldCache(innerBlock, props, interop)) return
- // 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
+ if (interop) {
+ innerBlock.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+ key = innerBlock.vnode!.type
} else {
innerBlock.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
- toCache = innerBlock
key = innerBlock.type
}
-
- innerCacheBlock(key, toCache)
+ innerCacheBlock(key, innerBlock)
}
- keepAliveInstance.activate = (instance, parentNode, anchor) => {
- current = instance
- activate(instance, parentNode, anchor)
- }
-
- keepAliveInstance.deactivate = instance => {
- current = undefined
- 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__) {
- warn(`KeepAlive should contain exactly one component child.`)
- }
- return 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) {
+ const pruneCache = (filter: (name: string) => boolean) => {
cache.forEach((cached, key) => {
const instance = getInstanceFromCache(cached)
if (!instance) return
})
}
- function pruneCacheEntry(key: CacheKey) {
+ const pruneCacheEntry = (key: CacheKey) => {
const cached = cache.get(key)!
resetCachedShapeFlag(cached)
{ flush: 'post', deep: true },
)
+ onMounted(cacheBlock)
+ onUpdated(cacheBlock)
+ onBeforeUnmount(() => {
+ 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 currentKey = isVaporComponent(current)
+ ? current.type
+ : current.vnode!.type
+ if (currentKey === key) {
+ // call deactivated hook
+ const da = instance.da
+ da && queuePostFlushCb(da)
+ return
+ }
+ }
+
+ remove(cached, storageContainer)
+ })
+ keptAliveScopes.forEach(scope => scope.stop())
+ keptAliveScopes.clear()
+ })
+
+ let children = slots.default()
+ if (isArray(children)) {
+ children = children.filter(child => !(child instanceof Comment))
+ if (children.length > 1) {
+ if (__DEV__) {
+ warn(`KeepAlive should contain exactly one component child.`)
+ }
+ return children
+ }
+ }
+
+ // inject hooks to DynamicFragment to cache components during updates
+ const injectKeepAliveHooks = (frag: DynamicFragment) => {
+ ;(frag.beforeTeardown || (frag.beforeTeardown = [])).push(
+ (oldKey, nodes, scope) => {
+ processFragment(frag)
+ keptAliveScopes.set(oldKey, scope)
+ return true
+ },
+ )
+ ;(frag.beforeMount || (frag.beforeMount = [])).push(() =>
+ cacheFragment(frag),
+ )
+ frag.getScope = key => {
+ const scope = keptAliveScopes.get(key)
+ if (scope) {
+ keptAliveScopes.delete(key)
+ return scope
+ }
+ }
+ }
+
+ // process shapeFlag
+ if (isVaporComponent(children)) {
+ children.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+ if (isAsyncWrapper(children)) {
+ injectKeepAliveHooks(children.block as DynamicFragment)
+ }
+ } else if (isInteropFragment(children)) {
+ children.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+ } else if (isDynamicFragment(children)) {
+ processFragment(children)
+ injectKeepAliveHooks(children)
+ if (isVaporComponent(children.nodes) && isAsyncWrapper(children.nodes)) {
+ injectKeepAliveHooks(children.nodes.block as DynamicFragment)
+ }
+ }
+
return children
},
})
-function getInnerBlock(block: Block): VaporComponentInstance | undefined {
+const shouldCache = (
+ block: GenericComponentInstance | VaporFragment,
+ props: KeepAliveProps,
+ interop: boolean = false,
+) => {
+ const isAsync = !interop && isAsyncWrapper(block as GenericComponentInstance)
+ const type = (
+ interop
+ ? (block as VaporFragment).vnode!.type
+ : (block as GenericComponentInstance).type
+ ) as GenericComponent & AsyncComponentInternalOptions
+
+ // return true to ensure hooks are injected into its block (DynamicFragment)
+ if (isAsync && !type.__asyncResolved) {
+ return true
+ }
+
+ const { include, exclude } = props
+ const name = getComponentName(isAsync ? type.__asyncResolved! : type)
+ return !(
+ (include && (!name || !matches(include, name))) ||
+ (exclude && name && matches(exclude, name))
+ )
+}
+
+const resetCachedShapeFlag = (
+ cached: VaporComponentInstance | VaporFragment,
+) => {
+ if (isVaporComponent(cached)) {
+ resetShapeFlag(cached)
+ } else {
+ resetShapeFlag(cached.vnode)
+ }
+}
+
+type InnerBlockResult =
+ | [VaporFragment, true]
+ | [VaporComponentInstance, false]
+ | [undefined, false]
+
+function getInnerBlock(block: Block): InnerBlockResult {
if (isVaporComponent(block)) {
- return block
+ return [block, false]
} else if (isInteropFragment(block)) {
- return block.vnode as any
+ return [block, true]
} else if (isFragment(block)) {
return getInnerBlock(block.nodes)
}
+ return [undefined, false]
}
function isInteropFragment(block: Block): block is VaporFragment {
return !!(isFragment(block) && block.vnode)
}
-function findInteropFragment(block: Block): VaporFragment | undefined {
- if (isInteropFragment(block)) {
- return block
- }
- if (isFragment(block)) {
- return findInteropFragment(block.nodes)
- }
-}
-
function getInstanceFromCache(
cached: VaporComponentInstance | VaporFragment,
): GenericComponentInstance {
remove,
} from './block'
import {
- type GenericComponentInstance,
type TransitionHooks,
type VNode,
- currentInstance,
- isKeepAlive,
queuePostFlushCb,
} from '@vue/runtime-dom'
import type { VaporComponentInstance } from './component'
import type { NodeRef } from './apiTemplateRef'
-import type { KeepAliveInstance } from './components/KeepAlive'
import {
applyTransitionHooks,
applyTransitionLeaveHooks,
current?: BlockFn
fallback?: BlockFn
anchorLabel?: string
- inKeepAlive?: boolean
- keptAliveScopes?: Map<any, EffectScope>
+
+ // get the kept-alive scope when used in keep-alive
+ getScope?: (key: any) => EffectScope | undefined
+
+ // hooks
+ beforeTeardown?: ((
+ oldKey: any,
+ nodes: Block,
+ scope: EffectScope,
+ ) => boolean)[]
+ beforeMount?: ((newKey: any, nodes: Block, scope: EffectScope) => void)[]
+ mounted?: ((nodes: Block, scope: EffectScope) => void)[]
constructor(anchorLabel?: string) {
super([])
const prevSub = setActiveSub()
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 (this.inKeepAlive) {
- ;(instance as KeepAliveInstance).processFragment(this)
- if (!this.keptAliveScopes) this.keptAliveScopes = new Map()
- this.keptAliveScopes.set(this.current, this.scope)
- } else {
+ let preserveScope = false
+ // if any of the hooks returns true the scope will be preserved
+ // for kept-alive component
+ if (this.beforeTeardown) {
+ preserveScope = this.beforeTeardown.some(hook =>
+ hook(this.current, this.nodes, this.scope!),
+ )
+ }
+ if (!preserveScope) {
this.scope.stop()
}
const mode = transition && transition.mode
if (mode) {
applyTransitionLeaveHooks(this.nodes, transition, () =>
- this.render(render, instance, transition, parent),
+ this.render(render, transition, parent),
)
parent && remove(this.nodes, parent)
if (mode === 'out-in') {
}
}
- this.render(render, instance, transition, parent)
+ this.render(render, transition, parent)
if (this.fallback) {
// set fallback for nested fragments
private render(
render: BlockFn | undefined,
- instance: GenericComponentInstance,
transition: VaporTransitionHooks | undefined,
parent: ParentNode | null,
) {
if (render) {
- // For KeepAlive, try to reuse the keepAlive scope for this key
- const scope =
- this.inKeepAlive && this.keptAliveScopes
- ? this.keptAliveScopes.get(this.current)
- : undefined
+ // try to reuse the kept-alive scope
+ const scope = this.getScope && this.getScope(this.current)
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 (this.inKeepAlive) {
- ;(instance as KeepAliveInstance).cacheFragment(this)
- }
+
if (transition) {
this.$transition = applyTransitionHooks(this.nodes, transition)
}
- if (parent) insert(this.nodes, parent, this.anchor)
+
+ if (this.beforeMount) {
+ this.beforeMount.forEach(hook =>
+ hook(this.current, this.nodes, this.scope!),
+ )
+ }
+
+ if (parent) {
+ insert(this.nodes, parent, this.anchor)
+ if (this.mounted) {
+ this.mounted.forEach(hook => hook(this.nodes, this.scope!))
+ }
+ }
} else {
this.scope = undefined
this.nodes = []
export function isFragment(val: NonNullable<unknown>): val is VaporFragment {
return val instanceof VaporFragment
}
+
+export function isDynamicFragment(
+ val: NonNullable<unknown>,
+): val is DynamicFragment {
+ return val instanceof DynamicFragment
+}