402 lines
12 KiB
TypeScript
402 lines
12 KiB
TypeScript
import {
|
|
ConcreteComponent,
|
|
getCurrentInstance,
|
|
SetupContext,
|
|
ComponentInternalInstance,
|
|
LifecycleHooks,
|
|
currentInstance,
|
|
getComponentName
|
|
} from '../component'
|
|
import { VNode, cloneVNode, isVNode, VNodeProps } from '../vnode'
|
|
import { warn } from '../warning'
|
|
import {
|
|
onBeforeUnmount,
|
|
injectHook,
|
|
onUnmounted,
|
|
onMounted,
|
|
onUpdated
|
|
} from '../apiLifecycle'
|
|
import {
|
|
isString,
|
|
isArray,
|
|
ShapeFlags,
|
|
remove,
|
|
invokeArrayFns
|
|
} from '@vue/shared'
|
|
import { watch } from '../apiWatch'
|
|
import {
|
|
RendererInternals,
|
|
queuePostRenderEffect,
|
|
MoveType,
|
|
RendererElement,
|
|
RendererNode,
|
|
invokeVNodeHook
|
|
} from '../renderer'
|
|
import { setTransitionHooks } from './BaseTransition'
|
|
import { ComponentRenderContext } from '../componentPublicInstance'
|
|
|
|
type MatchPattern = string | RegExp | string[] | RegExp[]
|
|
|
|
export interface KeepAliveProps {
|
|
include?: MatchPattern
|
|
exclude?: MatchPattern
|
|
max?: number | string
|
|
}
|
|
|
|
type CacheKey = string | number | ConcreteComponent
|
|
type Cache = Map<CacheKey, VNode>
|
|
type Keys = Set<CacheKey>
|
|
|
|
export interface KeepAliveContext extends ComponentRenderContext {
|
|
renderer: RendererInternals
|
|
activate: (
|
|
vnode: VNode,
|
|
container: RendererElement,
|
|
anchor: RendererNode | null,
|
|
isSVG: boolean,
|
|
optimized: boolean
|
|
) => void
|
|
deactivate: (vnode: VNode) => void
|
|
}
|
|
|
|
export const isKeepAlive = (vnode: VNode): boolean =>
|
|
(vnode.type as any).__isKeepAlive
|
|
|
|
const KeepAliveImpl = {
|
|
name: `KeepAlive`,
|
|
|
|
// Marker for special handling inside the renderer. We are not using a ===
|
|
// check directly on KeepAlive in the renderer, because importing it directly
|
|
// would prevent it from being tree-shaken.
|
|
__isKeepAlive: true,
|
|
|
|
props: {
|
|
include: [String, RegExp, Array],
|
|
exclude: [String, RegExp, Array],
|
|
max: [String, Number]
|
|
},
|
|
|
|
setup(props: KeepAliveProps, { slots }: SetupContext) {
|
|
const instance = getCurrentInstance()!
|
|
// KeepAlive communicates with the instantiated renderer via the
|
|
// ctx where the renderer passes in its internals,
|
|
// and the KeepAlive instance exposes activate/deactivate implementations.
|
|
// The whole point of this is to avoid importing KeepAlive directly in the
|
|
// renderer to facilitate tree-shaking.
|
|
const sharedContext = instance.ctx as KeepAliveContext
|
|
|
|
// if the internal renderer is not registered, it indicates that this is server-side rendering,
|
|
// for KeepAlive, we just need to render its children
|
|
if (!sharedContext.renderer) {
|
|
return slots.default
|
|
}
|
|
|
|
const cache: Cache = new Map()
|
|
const keys: Keys = new Set()
|
|
let current: VNode | null = null
|
|
|
|
const parentSuspense = instance.suspense
|
|
|
|
const {
|
|
renderer: {
|
|
p: patch,
|
|
m: move,
|
|
um: _unmount,
|
|
o: { createElement }
|
|
}
|
|
} = sharedContext
|
|
const storageContainer = createElement('div')
|
|
|
|
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
|
|
const instance = vnode.component!
|
|
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
|
|
// in case props have changed
|
|
patch(
|
|
instance.vnode,
|
|
vnode,
|
|
container,
|
|
anchor,
|
|
instance,
|
|
parentSuspense,
|
|
isSVG,
|
|
vnode.slotScopeIds,
|
|
optimized
|
|
)
|
|
queuePostRenderEffect(() => {
|
|
instance.isDeactivated = false
|
|
if (instance.a) {
|
|
invokeArrayFns(instance.a)
|
|
}
|
|
const vnodeHook = vnode.props && vnode.props.onVnodeMounted
|
|
if (vnodeHook) {
|
|
invokeVNodeHook(vnodeHook, instance.parent, vnode)
|
|
}
|
|
}, parentSuspense)
|
|
}
|
|
|
|
sharedContext.deactivate = (vnode: VNode) => {
|
|
const instance = vnode.component!
|
|
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
|
|
queuePostRenderEffect(() => {
|
|
if (instance.da) {
|
|
invokeArrayFns(instance.da)
|
|
}
|
|
const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
|
|
if (vnodeHook) {
|
|
invokeVNodeHook(vnodeHook, instance.parent, vnode)
|
|
}
|
|
instance.isDeactivated = true
|
|
}, parentSuspense)
|
|
}
|
|
|
|
function unmount(vnode: VNode) {
|
|
// reset the shapeFlag so it can be properly unmounted
|
|
resetShapeFlag(vnode)
|
|
_unmount(vnode, instance, parentSuspense)
|
|
}
|
|
|
|
function pruneCache(filter?: (name: string) => boolean) {
|
|
cache.forEach((vnode, key) => {
|
|
const name = getComponentName(vnode.type as ConcreteComponent)
|
|
if (name && (!filter || !filter(name))) {
|
|
pruneCacheEntry(key)
|
|
}
|
|
})
|
|
}
|
|
|
|
function pruneCacheEntry(key: CacheKey) {
|
|
const cached = cache.get(key) as VNode
|
|
if (!current || cached.type !== current.type) {
|
|
unmount(cached)
|
|
} else if (current) {
|
|
// current active instance should no longer be kept-alive.
|
|
// we can't unmount it now but it might be later, so reset its flag now.
|
|
resetShapeFlag(current)
|
|
}
|
|
cache.delete(key)
|
|
keys.delete(key)
|
|
}
|
|
|
|
// prune cache on include/exclude prop change
|
|
watch(
|
|
() => [props.include, props.exclude],
|
|
([include, exclude]) => {
|
|
include && pruneCache(name => matches(include, name))
|
|
exclude && pruneCache(name => !matches(exclude, name))
|
|
},
|
|
// prune post-render after `current` has been updated
|
|
{ flush: 'post', deep: true }
|
|
)
|
|
|
|
// cache sub tree after render
|
|
let pendingCacheKey: CacheKey | null = null
|
|
const cacheSubtree = () => {
|
|
// fix #1621, the pendingCacheKey could be 0
|
|
if (pendingCacheKey != null) {
|
|
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
|
|
}
|
|
}
|
|
onMounted(cacheSubtree)
|
|
onUpdated(cacheSubtree)
|
|
|
|
onBeforeUnmount(() => {
|
|
cache.forEach(cached => {
|
|
const { subTree, suspense } = instance
|
|
const vnode = getInnerChild(subTree)
|
|
if (cached.type === vnode.type) {
|
|
// current instance will be unmounted as part of keep-alive's unmount
|
|
resetShapeFlag(vnode)
|
|
// but invoke its deactivated hook here
|
|
const da = vnode.component!.da
|
|
da && queuePostRenderEffect(da, suspense)
|
|
return
|
|
}
|
|
unmount(cached)
|
|
})
|
|
})
|
|
|
|
return () => {
|
|
pendingCacheKey = null
|
|
|
|
if (!slots.default) {
|
|
return null
|
|
}
|
|
|
|
const children = slots.default()
|
|
const rawVNode = children[0]
|
|
if (children.length > 1) {
|
|
if (__DEV__) {
|
|
warn(`KeepAlive should contain exactly one component child.`)
|
|
}
|
|
current = null
|
|
return children
|
|
} else if (
|
|
!isVNode(rawVNode) ||
|
|
(!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
|
|
!(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
|
|
) {
|
|
current = null
|
|
return rawVNode
|
|
}
|
|
|
|
let vnode = getInnerChild(rawVNode)
|
|
const comp = vnode.type as ConcreteComponent
|
|
const name = getComponentName(comp)
|
|
const { include, exclude, max } = props
|
|
|
|
if (
|
|
(include && (!name || !matches(include, name))) ||
|
|
(exclude && name && matches(exclude, name))
|
|
) {
|
|
current = vnode
|
|
return rawVNode
|
|
}
|
|
|
|
const key = vnode.key == null ? comp : vnode.key
|
|
const cachedVNode = cache.get(key)
|
|
|
|
// clone vnode if it's reused because we are going to mutate it
|
|
if (vnode.el) {
|
|
vnode = cloneVNode(vnode)
|
|
if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
|
|
rawVNode.ssContent = vnode
|
|
}
|
|
}
|
|
// #1513 it's possible for the returned vnode to be cloned due to attr
|
|
// fallthrough or scopeId, so the vnode here may not be the final vnode
|
|
// that is mounted. Instead of caching it directly, we store the pending
|
|
// key and cache `instance.subTree` (the normalized vnode) in
|
|
// beforeMount/beforeUpdate hooks.
|
|
pendingCacheKey = key
|
|
|
|
if (cachedVNode) {
|
|
// copy over mounted state
|
|
vnode.el = cachedVNode.el
|
|
vnode.component = cachedVNode.component
|
|
if (vnode.transition) {
|
|
// recursively update transition hooks on subTree
|
|
setTransitionHooks(vnode, vnode.transition!)
|
|
}
|
|
// avoid vnode being mounted as fresh
|
|
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
|
|
// make this key the freshest
|
|
keys.delete(key)
|
|
keys.add(key)
|
|
} else {
|
|
keys.add(key)
|
|
// prune oldest entry
|
|
if (max && keys.size > parseInt(max as string, 10)) {
|
|
pruneCacheEntry(keys.values().next().value)
|
|
}
|
|
}
|
|
// avoid vnode being unmounted
|
|
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
|
|
|
|
current = vnode
|
|
return rawVNode
|
|
}
|
|
}
|
|
}
|
|
|
|
// export the public type for h/tsx inference
|
|
// also to avoid inline import() in generated d.ts files
|
|
export const KeepAlive = (KeepAliveImpl as any) as {
|
|
__isKeepAlive: true
|
|
new (): {
|
|
$props: VNodeProps & KeepAliveProps
|
|
}
|
|
}
|
|
|
|
function matches(pattern: MatchPattern, name: string): boolean {
|
|
if (isArray(pattern)) {
|
|
return pattern.some((p: string | RegExp) => matches(p, name))
|
|
} else if (isString(pattern)) {
|
|
return pattern.split(',').indexOf(name) > -1
|
|
} else if (pattern.test) {
|
|
return pattern.test(name)
|
|
}
|
|
/* istanbul ignore next */
|
|
return false
|
|
}
|
|
|
|
export function onActivated(
|
|
hook: Function,
|
|
target?: ComponentInternalInstance | null
|
|
) {
|
|
registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target)
|
|
}
|
|
|
|
export function onDeactivated(
|
|
hook: Function,
|
|
target?: ComponentInternalInstance | null
|
|
) {
|
|
registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target)
|
|
}
|
|
|
|
function registerKeepAliveHook(
|
|
hook: Function & { __wdc?: Function },
|
|
type: LifecycleHooks,
|
|
target: ComponentInternalInstance | null = currentInstance
|
|
) {
|
|
// cache the deactivate branch check wrapper for injected hooks so the same
|
|
// hook can be properly deduped by the scheduler. "__wdc" stands for "with
|
|
// deactivation check".
|
|
const wrappedHook =
|
|
hook.__wdc ||
|
|
(hook.__wdc = () => {
|
|
// only fire the hook if the target instance is NOT in a deactivated branch.
|
|
let current: ComponentInternalInstance | null = target
|
|
while (current) {
|
|
if (current.isDeactivated) {
|
|
return
|
|
}
|
|
current = current.parent
|
|
}
|
|
hook()
|
|
})
|
|
injectHook(type, wrappedHook, target)
|
|
// In addition to registering it on the target instance, we walk up the parent
|
|
// chain and register it on all ancestor instances that are keep-alive roots.
|
|
// This avoids the need to walk the entire component tree when invoking these
|
|
// hooks, and more importantly, avoids the need to track child components in
|
|
// arrays.
|
|
if (target) {
|
|
let current = target.parent
|
|
while (current && current.parent) {
|
|
if (isKeepAlive(current.parent.vnode)) {
|
|
injectToKeepAliveRoot(wrappedHook, type, target, current)
|
|
}
|
|
current = current.parent
|
|
}
|
|
}
|
|
}
|
|
|
|
function injectToKeepAliveRoot(
|
|
hook: Function & { __weh?: Function },
|
|
type: LifecycleHooks,
|
|
target: ComponentInternalInstance,
|
|
keepAliveRoot: ComponentInternalInstance
|
|
) {
|
|
// injectHook wraps the original for error handling, so make sure to remove
|
|
// the wrapped version.
|
|
const injected = injectHook(type, hook, keepAliveRoot, true /* prepend */)
|
|
onUnmounted(() => {
|
|
remove(keepAliveRoot[type]!, injected)
|
|
}, target)
|
|
}
|
|
|
|
function resetShapeFlag(vnode: VNode) {
|
|
let shapeFlag = vnode.shapeFlag
|
|
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
|
|
shapeFlag -= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
|
|
}
|
|
if (shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
|
|
shapeFlag -= ShapeFlags.COMPONENT_KEPT_ALIVE
|
|
}
|
|
vnode.shapeFlag = shapeFlag
|
|
}
|
|
|
|
function getInnerChild(vnode: VNode) {
|
|
return vnode.shapeFlag & ShapeFlags.SUSPENSE ? vnode.ssContent! : vnode
|
|
}
|