import { defineVaporComponent } from '../apiDefineComponent'
import { ShapeFlags, invokeArrayFns, isArray } from '@vue/shared'
import { createElement } from '../dom/node'
-import {
- type DynamicFragment,
- type VaporFragment,
- isDynamicFragment,
- isFragment,
-} from '../fragment'
+import { type VaporFragment, isDynamicFragment, isFragment } from '../fragment'
import type { EffectScope } from '@vue/reactivity'
+export interface KeepAliveContext {
+ processShapeFlag(block: Block): boolean
+ cacheBlock(): void
+ cacheScope(key: any, scope: EffectScope): void
+ getScope(key: any): EffectScope | undefined
+}
+
+export let currentKeepAliveCtx: KeepAliveContext | null = null
+
+export function setCurrentKeepAliveCtx(
+ ctx: KeepAliveContext | null,
+): KeepAliveContext | null {
+ try {
+ return currentKeepAliveCtx
+ } finally {
+ currentKeepAliveCtx = ctx
+ }
+}
+
export interface KeepAliveInstance extends VaporComponentInstance {
ctx: {
activate: (
)
}
- const processFragment = (frag: DynamicFragment) => {
- const [innerBlock, interop] = getInnerBlock(frag.nodes)
+ const processShapeFlag = (block: Block): boolean => {
+ const [innerBlock, interop] = getInnerBlock(block)
if (!innerBlock || !shouldCache(innerBlock!, props, interop)) return false
if (interop) {
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.onBeforeTeardown || (frag.onBeforeTeardown = [])).push(
- (oldKey, { retainScope }) => {
- // if the fragment's nodes include a component that should be cached
- // call retainScope() to avoid stopping the fragment's scope
- if (processFragment(frag)) {
- keptAliveScopes.set(oldKey, frag.scope!)
- retainScope()
- }
- },
- )
- ;(frag.onBeforeMount || (frag.onBeforeMount = [])).push(() => {
- processFragment(frag)
- // recursively inject hooks to nested DynamicFragments.
- // this is necessary for cases like v-if > dynamic component where
- // v-if starts as false - the nested DynamicFragment doesn't exist
- // during initial setup, so we must inject hooks when v-if becomes true.
- processChildren(frag.nodes)
- })
- // This ensures caching happens after renderBranch completes,
- // since Vue's onUpdated fires before the deferred rendering finishes.
- ;(frag.onUpdated || (frag.onUpdated = [])).push(() => {
- if (frag.$transition && frag.$transition.mode === 'out-in') {
- cacheBlock()
- }
- })
- frag.getScope = key => {
+ const keepAliveCtx: KeepAliveContext = {
+ processShapeFlag,
+ cacheBlock,
+ cacheScope(key, scope) {
+ keptAliveScopes.set(key, scope)
+ },
+ getScope(key) {
const scope = keptAliveScopes.get(key)
if (scope) {
keptAliveScopes.delete(key)
return scope
}
- }
+ },
}
- // for unresolved async wrapper, we need to watch the __asyncResolved
- // property and cache the resolved component once it resolves.
- const watchAsyncResolve = (instance: VaporComponentInstance) => {
- if (!instance.type.__asyncResolved) {
- watch(
- () => instance.type.__asyncResolved,
- resolved => {
- if (resolved) cacheBlock()
- },
- { once: true },
- )
- }
- }
+ const prevCtx = setCurrentKeepAliveCtx(keepAliveCtx)
+ let children = slots.default()
+ setCurrentKeepAliveCtx(prevCtx)
- // recursively inject hooks to nested DynamicFragments and handle AsyncWrapper
- const processChildren = (block: Block): void => {
- // handle async wrapper
- if (isVaporComponent(block) && isAsyncWrapper(block)) {
- watchAsyncResolve(block)
- // block.block is a DynamicFragment
- processChildren(block.block)
- } else if (isDynamicFragment(block)) {
- // avoid injecting hooks multiple times
- if (!block.getScope) {
- // DynamicFragment triggers processFragment via onBeforeMount hook,
- // which correctly handles shapeFlag marking for inner components.
- injectKeepAliveHooks(block)
- if (block.nodes) processFragment(block)
+ if (isArray(children)) {
+ children = children.filter(child => !(child instanceof Comment))
+ if (children.length > 1) {
+ if (__DEV__) {
+ warn(`KeepAlive should contain exactly one component child.`)
}
- processChildren(block.nodes)
- }
- }
-
- if (isVaporComponent(children)) {
- children.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
- processChildren(children)
- } else if (isFragment(children)) {
- // vdom interop
- if (children.vnode) {
- children.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
- } else {
- processChildren(children)
+ return children
}
}
) as GenericComponent & AsyncComponentInternalOptions
// for unresolved async components, don't cache yet
- // caching will be handled by the watcher in watchAsyncResolve
+ // caching will be handled by AsyncWrapper calling keepAliveCtx.cacheBlock()
if (isAsync && !type.__asyncResolved) {
return false
}
import { isArray } from '@vue/shared'
import { renderEffect } from './renderEffect'
import { currentSlotOwner, setCurrentSlotOwner } from './componentSlots'
+import {
+ type KeepAliveContext,
+ currentKeepAliveCtx,
+ setCurrentKeepAliveCtx,
+} from './components/KeepAlive'
export class VaporFragment<
T extends Block = Block,
// set ref for async wrapper
setAsyncRef?: (instance: VaporComponentInstance) => void
- // get the kept-alive scope when used in keep-alive
- getScope?: (key: any) => EffectScope | undefined
-
- // hooks
- onBeforeTeardown?: ((
- oldKey: any,
- context: { retainScope: () => void },
- ) => void)[]
- onBeforeMount?: ((newKey: any, nodes: Block, scope: EffectScope) => void)[]
+ keepAliveCtx: KeepAliveContext | null
slotOwner: VaporComponentInstance | null
super([])
this.keyed = keyed
this.slotOwner = currentSlotOwner
+ this.keepAliveCtx = currentKeepAliveCtx
if (isHydrating) {
this.anchorLabel = anchorLabel
locateHydrationNode()
// teardown previous branch
if (this.scope) {
let retainScope = false
- const context = {
- retainScope: () => (retainScope = true),
- }
- if (this.onBeforeTeardown) {
- for (const teardown of this.onBeforeTeardown) {
- teardown(prevKey, context)
- }
+ const keepAliveCtx = this.keepAliveCtx
+
+ // if keepAliveCtx exists and processShapeFlag returns true,
+ // cache the scope and retain it
+ if (keepAliveCtx && keepAliveCtx.processShapeFlag(this.nodes)) {
+ keepAliveCtx.cacheScope(prevKey, this.scope)
+ retainScope = true
}
+
if (!retainScope) {
this.scope.stop()
}
instance: GenericComponentInstance | null,
): void {
if (render) {
+ const keepAliveCtx = this.keepAliveCtx
// try to reuse the kept-alive scope
- const scope = this.getScope && this.getScope(this.current)
+ const scope = keepAliveCtx && keepAliveCtx.getScope(this.current)
if (scope) {
this.scope = scope
} else {
// restore slot owner
const prevOwner = setCurrentSlotOwner(this.slotOwner)
+ // set currentKeepAliveCtx so nested DynamicFragments and components can capture it
+ const prevCtx = setCurrentKeepAliveCtx(keepAliveCtx)
// switch current instance to parent instance during update
// ensure that the parent instance is correct for nested components
const prev = parent && instance ? setCurrentInstance(instance) : undefined
this.nodes = this.scope.run(render) || []
if (prev !== undefined) setCurrentInstance(...prev)
+ setCurrentKeepAliveCtx(prevCtx)
setCurrentSlotOwner(prevOwner)
// set key on nodes
this.$transition = applyTransitionHooks(this.nodes, transition)
}
- if (this.onBeforeMount) {
- this.onBeforeMount.forEach(hook =>
- hook(this.current, this.nodes, this.scope!),
- )
+ // call processShapeFlag to mark shapeFlag before mounting
+ if (keepAliveCtx) {
+ keepAliveCtx.processShapeFlag(this.nodes)
}
if (parent) {
}
insert(this.nodes, parent, this.anchor)
+
+ // For out-in transition, call cacheBlock after renderBranch completes
+ // because KeepAlive's onUpdated fires before the deferred rendering finishes
+ if (keepAliveCtx && transition && transition.mode === 'out-in') {
+ keepAliveCtx.cacheBlock()
+ }
+
if (this.onUpdated) {
this.onUpdated.forEach(hook => hook(this.nodes))
}