| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395 |
- 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,
- inheritRef: true,
- props: {
- include: [String, RegExp, Array],
- exclude: [String, RegExp, Array],
- max: [String, Number]
- },
- setup(props: KeepAliveProps, { slots }: SetupContext) {
- const cache: Cache = new Map()
- const keys: Keys = new Set()
- let current: VNode | null = null
- const instance = getCurrentInstance()!
- const parentSuspense = instance.suspense
- // 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
- 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,
- 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
- }
|