| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- import {
- Component,
- getCurrentInstance,
- FunctionalComponent,
- SetupContext,
- ComponentInternalInstance,
- LifecycleHooks,
- currentInstance
- } from '../component'
- import { VNode, cloneVNode, isVNode, VNodeProps } from '../vnode'
- import { warn } from '../warning'
- import { onBeforeUnmount, injectHook, onUnmounted } from '../apiLifecycle'
- import {
- isString,
- isArray,
- ShapeFlags,
- remove,
- invokeArrayFns
- } from '@vue/shared'
- import { watch } from '../apiWatch'
- import { SuspenseBoundary } from './Suspense'
- import {
- RendererInternals,
- queuePostRenderEffect,
- MoveType,
- RendererElement,
- RendererNode
- } from '../renderer'
- import { setTransitionHooks } from './BaseTransition'
- type MatchPattern = string | RegExp | string[] | RegExp[]
- export interface KeepAliveProps {
- include?: MatchPattern
- exclude?: MatchPattern
- max?: number | string
- }
- type CacheKey = string | number | Component
- type Cache = Map<CacheKey, VNode>
- type Keys = Set<CacheKey>
- export interface KeepAliveSink {
- renderer: RendererInternals
- parentSuspense: SuspenseBoundary | null
- 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 cache: Cache = new Map()
- const keys: Keys = new Set()
- let current: VNode | null = null
- const instance = getCurrentInstance()!
- // KeepAlive communicates with the instantiated renderer via the "sink"
- // where the renderer passes in platform-specific functions, 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 sink = instance.sink as KeepAliveSink
- const {
- renderer: {
- p: patch,
- m: move,
- um: _unmount,
- o: { createElement }
- },
- parentSuspense
- } = sink
- const storageContainer = createElement('div')
- sink.activate = (vnode, container, anchor, isSVG, optimized) => {
- const child = vnode.component!
- move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
- // in case props have changed
- patch(
- child.vnode,
- vnode,
- container,
- anchor,
- instance,
- parentSuspense,
- isSVG,
- optimized
- )
- queuePostRenderEffect(() => {
- child.isDeactivated = false
- if (child.a) {
- invokeArrayFns(child.a)
- }
- }, parentSuspense)
- }
- sink.deactivate = (vnode: VNode) => {
- move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
- queuePostRenderEffect(() => {
- const component = vnode.component!
- if (component.da) {
- invokeArrayFns(component.da)
- }
- component.isDeactivated = true
- }, parentSuspense)
- }
- function unmount(vnode: VNode) {
- // reset the shapeFlag so it can be properly unmounted
- vnode.shapeFlag = ShapeFlags.STATEFUL_COMPONENT
- _unmount(vnode, instance, parentSuspense)
- }
- function pruneCache(filter?: (name: string) => boolean) {
- cache.forEach((vnode, key) => {
- const name = getName(vnode.type as Component)
- 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.
- current.shapeFlag = ShapeFlags.STATEFUL_COMPONENT
- }
- cache.delete(key)
- keys.delete(key)
- }
- watch(
- () => [props.include, props.exclude],
- ([include, exclude]) => {
- include && pruneCache(name => matches(include, name))
- exclude && pruneCache(name => matches(exclude, name))
- }
- )
- onBeforeUnmount(() => {
- cache.forEach(unmount)
- })
- return () => {
- if (!slots.default) {
- return null
- }
- const children = slots.default()
- let vnode = children[0]
- if (children.length > 1) {
- if (__DEV__) {
- warn(`KeepAlive should contain exactly one component child.`)
- }
- current = null
- return children
- } else if (
- !isVNode(vnode) ||
- !(vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT)
- ) {
- current = null
- return vnode
- }
- const comp = vnode.type as Component
- const name = getName(comp)
- const { include, exclude, max } = props
- if (
- (include && (!name || !matches(include, name))) ||
- (exclude && name && matches(exclude, name))
- ) {
- return vnode
- }
- 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)
- }
- cache.set(key, vnode)
- 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(Array.from(keys)[0])
- }
- }
- // avoid vnode being unmounted
- vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
- current = vnode
- return vnode
- }
- }
- }
- // 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 {
- new (): {
- $props: VNodeProps & KeepAliveProps
- }
- }
- function getName(comp: Component): string | void {
- return (comp as FunctionalComponent).displayName || comp.name
- }
- function matches(pattern: MatchPattern, name: string): boolean {
- if (isArray(pattern)) {
- return (pattern as any).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,
- type: LifecycleHooks,
- target: ComponentInternalInstance,
- keepAliveRoot: ComponentInternalInstance
- ) {
- injectHook(type, hook, keepAliveRoot, true /* prepend */)
- onUnmounted(() => {
- remove(keepAliveRoot[type]!, hook)
- }, target)
- }
|