feat(core): keep-alive
This commit is contained in:
parent
083296ead6
commit
c6cbca25fe
@ -111,7 +111,7 @@ describe('component: proxy', () => {
|
|||||||
expect(`Attempting to mutate public property "$data"`).toHaveBeenWarned()
|
expect(`Attempting to mutate public property "$data"`).toHaveBeenWarned()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('user', async () => {
|
it('sink', async () => {
|
||||||
const app = createApp()
|
const app = createApp()
|
||||||
let instance: ComponentInternalInstance
|
let instance: ComponentInternalInstance
|
||||||
let instanceProxy: any
|
let instanceProxy: any
|
||||||
@ -127,6 +127,6 @@ describe('component: proxy', () => {
|
|||||||
app.mount(Comp, nodeOps.createElement('div'))
|
app.mount(Comp, nodeOps.createElement('div'))
|
||||||
instanceProxy.foo = 1
|
instanceProxy.foo = 1
|
||||||
expect(instanceProxy.foo).toBe(1)
|
expect(instanceProxy.foo).toBe(1)
|
||||||
expect(instance!.user.foo).toBe(1)
|
expect(instance!.sink.foo).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -132,7 +132,7 @@ describe('vnode', () => {
|
|||||||
mounted.el = {}
|
mounted.el = {}
|
||||||
const normalized = normalizeVNode(mounted)
|
const normalized = normalizeVNode(mounted)
|
||||||
expect(normalized).not.toBe(mounted)
|
expect(normalized).not.toBe(mounted)
|
||||||
expect(normalized).toEqual({ ...mounted, el: null })
|
expect(normalized).toEqual(mounted)
|
||||||
|
|
||||||
// primitive types
|
// primitive types
|
||||||
expect(normalizeVNode('foo')).toMatchObject({ type: Text, children: `foo` })
|
expect(normalizeVNode('foo')).toMatchObject({ type: Text, children: `foo` })
|
||||||
@ -158,20 +158,6 @@ describe('vnode', () => {
|
|||||||
expect(cloned2).toEqual(node2)
|
expect(cloned2).toEqual(node2)
|
||||||
expect(cloneVNode(node2)).toEqual(node2)
|
expect(cloneVNode(node2)).toEqual(node2)
|
||||||
expect(cloneVNode(node2)).toEqual(cloned2)
|
expect(cloneVNode(node2)).toEqual(cloned2)
|
||||||
|
|
||||||
// should reset mounted state
|
|
||||||
const node3 = createVNode('div', { foo: 1 }, [node1])
|
|
||||||
node3.el = {}
|
|
||||||
node3.anchor = {}
|
|
||||||
node3.component = {} as any
|
|
||||||
node3.suspense = {} as any
|
|
||||||
expect(cloneVNode(node3)).toEqual({
|
|
||||||
...node3,
|
|
||||||
el: null,
|
|
||||||
anchor: null,
|
|
||||||
component: null,
|
|
||||||
suspense: null
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('mergeProps', () => {
|
describe('mergeProps', () => {
|
||||||
|
@ -9,14 +9,17 @@ import { callWithAsyncErrorHandling, ErrorTypeStrings } from './errorHandling'
|
|||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
import { capitalize } from '@vue/shared'
|
import { capitalize } from '@vue/shared'
|
||||||
import { pauseTracking, resumeTracking, DebuggerEvent } from '@vue/reactivity'
|
import { pauseTracking, resumeTracking, DebuggerEvent } from '@vue/reactivity'
|
||||||
|
import { registerKeepAliveHook } from './keepAlive'
|
||||||
|
|
||||||
function injectHook(
|
export function injectHook(
|
||||||
type: LifecycleHooks,
|
type: LifecycleHooks,
|
||||||
hook: Function,
|
hook: Function,
|
||||||
target: ComponentInternalInstance | null
|
target: ComponentInternalInstance | null = currentInstance,
|
||||||
|
prepend: boolean = false
|
||||||
) {
|
) {
|
||||||
if (target) {
|
if (target) {
|
||||||
;(target[type] || (target[type] = [])).push((...args: unknown[]) => {
|
const hooks = target[type] || (target[type] = [])
|
||||||
|
const wrappedHook = (...args: unknown[]) => {
|
||||||
if (target.isUnmounted) {
|
if (target.isUnmounted) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -31,7 +34,12 @@ function injectHook(
|
|||||||
setCurrentInstance(null)
|
setCurrentInstance(null)
|
||||||
resumeTracking()
|
resumeTracking()
|
||||||
return res
|
return res
|
||||||
})
|
}
|
||||||
|
if (prepend) {
|
||||||
|
hooks.unshift(wrappedHook)
|
||||||
|
} else {
|
||||||
|
hooks.push(wrappedHook)
|
||||||
|
}
|
||||||
} else if (__DEV__) {
|
} else if (__DEV__) {
|
||||||
const apiName = `on${capitalize(
|
const apiName = `on${capitalize(
|
||||||
ErrorTypeStrings[type].replace(/ hook$/, '')
|
ErrorTypeStrings[type].replace(/ hook$/, '')
|
||||||
@ -48,7 +56,7 @@ function injectHook(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createHook = <T extends Function = () => any>(
|
export const createHook = <T extends Function = () => any>(
|
||||||
lifecycle: LifecycleHooks
|
lifecycle: LifecycleHooks
|
||||||
) => (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
|
) => (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
|
||||||
injectHook(lifecycle, hook, target)
|
injectHook(lifecycle, hook, target)
|
||||||
@ -76,3 +84,17 @@ export type ErrorCapturedHook = (
|
|||||||
export const onErrorCaptured = createHook<ErrorCapturedHook>(
|
export const onErrorCaptured = createHook<ErrorCapturedHook>(
|
||||||
LifecycleHooks.ERROR_CAPTURED
|
LifecycleHooks.ERROR_CAPTURED
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
@ -26,6 +26,8 @@ import {
|
|||||||
onRenderTracked,
|
onRenderTracked,
|
||||||
onBeforeUnmount,
|
onBeforeUnmount,
|
||||||
onUnmounted,
|
onUnmounted,
|
||||||
|
onActivated,
|
||||||
|
onDeactivated,
|
||||||
onRenderTriggered,
|
onRenderTriggered,
|
||||||
DebuggerHook,
|
DebuggerHook,
|
||||||
ErrorCapturedHook
|
ErrorCapturedHook
|
||||||
@ -226,8 +228,8 @@ export function applyOptions(
|
|||||||
mounted,
|
mounted,
|
||||||
beforeUpdate,
|
beforeUpdate,
|
||||||
updated,
|
updated,
|
||||||
// TODO activated
|
activated,
|
||||||
// TODO deactivated
|
deactivated,
|
||||||
beforeUnmount,
|
beforeUnmount,
|
||||||
unmounted,
|
unmounted,
|
||||||
renderTracked,
|
renderTracked,
|
||||||
@ -377,6 +379,12 @@ export function applyOptions(
|
|||||||
if (updated) {
|
if (updated) {
|
||||||
onUpdated(updated.bind(ctx))
|
onUpdated(updated.bind(ctx))
|
||||||
}
|
}
|
||||||
|
if (activated) {
|
||||||
|
onActivated(activated.bind(ctx))
|
||||||
|
}
|
||||||
|
if (deactivated) {
|
||||||
|
onDeactivated(deactivated.bind(ctx))
|
||||||
|
}
|
||||||
if (errorCaptured) {
|
if (errorCaptured) {
|
||||||
onErrorCaptured(errorCaptured.bind(ctx))
|
onErrorCaptured(errorCaptured.bind(ctx))
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,7 @@ export interface FunctionalComponent<P = {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Component = ComponentOptions | FunctionalComponent
|
export type Component = ComponentOptions | FunctionalComponent
|
||||||
|
export { ComponentOptions }
|
||||||
|
|
||||||
type LifecycleHook = Function[] | null
|
type LifecycleHook = Function[] | null
|
||||||
|
|
||||||
@ -89,13 +90,10 @@ export interface ComponentInternalInstance {
|
|||||||
// after initialized (e.g. inline handlers)
|
// after initialized (e.g. inline handlers)
|
||||||
renderCache: (Function | VNode)[] | null
|
renderCache: (Function | VNode)[] | null
|
||||||
|
|
||||||
|
// assets for fast resolution
|
||||||
components: Record<string, Component>
|
components: Record<string, Component>
|
||||||
directives: Record<string, Directive>
|
directives: Record<string, Directive>
|
||||||
|
|
||||||
asyncDep: Promise<any> | null
|
|
||||||
asyncResult: unknown
|
|
||||||
asyncResolved: boolean
|
|
||||||
|
|
||||||
// the rest are only for stateful components
|
// the rest are only for stateful components
|
||||||
renderContext: Data
|
renderContext: Data
|
||||||
data: Data
|
data: Data
|
||||||
@ -108,11 +106,17 @@ export interface ComponentInternalInstance {
|
|||||||
refs: Data
|
refs: Data
|
||||||
emit: Emit
|
emit: Emit
|
||||||
|
|
||||||
// user namespace
|
// suspense related
|
||||||
user: { [key: string]: any }
|
asyncDep: Promise<any> | null
|
||||||
|
asyncResult: unknown
|
||||||
|
asyncResolved: boolean
|
||||||
|
|
||||||
|
// storage for any extra properties
|
||||||
|
sink: { [key: string]: any }
|
||||||
|
|
||||||
// lifecycle
|
// lifecycle
|
||||||
isUnmounted: boolean
|
isUnmounted: boolean
|
||||||
|
isDeactivated: boolean
|
||||||
[LifecycleHooks.BEFORE_CREATE]: LifecycleHook
|
[LifecycleHooks.BEFORE_CREATE]: LifecycleHook
|
||||||
[LifecycleHooks.CREATED]: LifecycleHook
|
[LifecycleHooks.CREATED]: LifecycleHook
|
||||||
[LifecycleHooks.BEFORE_MOUNT]: LifecycleHook
|
[LifecycleHooks.BEFORE_MOUNT]: LifecycleHook
|
||||||
@ -173,11 +177,13 @@ export function createComponentInstance(
|
|||||||
asyncResolved: false,
|
asyncResolved: false,
|
||||||
|
|
||||||
// user namespace for storing whatever the user assigns to `this`
|
// user namespace for storing whatever the user assigns to `this`
|
||||||
user: {},
|
// can also be used as a wildcard storage for ad-hoc injections internally
|
||||||
|
sink: {},
|
||||||
|
|
||||||
// lifecycle hooks
|
// lifecycle hooks
|
||||||
// not using enums here because it results in computed properties
|
// not using enums here because it results in computed properties
|
||||||
isUnmounted: false,
|
isUnmounted: false,
|
||||||
|
isDeactivated: false,
|
||||||
bc: null,
|
bc: null,
|
||||||
c: null,
|
c: null,
|
||||||
bm: null,
|
bm: null,
|
||||||
|
@ -73,7 +73,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
|||||||
propsProxy,
|
propsProxy,
|
||||||
accessCache,
|
accessCache,
|
||||||
type,
|
type,
|
||||||
user
|
sink
|
||||||
} = target
|
} = target
|
||||||
// fast path for unscopables when using `with` block
|
// fast path for unscopables when using `with` block
|
||||||
if (__RUNTIME_COMPILE__ && (key as any) === Symbol.unscopables) {
|
if (__RUNTIME_COMPILE__ && (key as any) === Symbol.unscopables) {
|
||||||
@ -128,8 +128,8 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
|||||||
return instanceWatch.bind(target)
|
return instanceWatch.bind(target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasOwn(user, key)) {
|
if (hasOwn(sink, key)) {
|
||||||
return user[key]
|
return sink[key]
|
||||||
} else if (__DEV__ && currentRenderingInstance != null) {
|
} else if (__DEV__ && currentRenderingInstance != null) {
|
||||||
warn(
|
warn(
|
||||||
`Property ${JSON.stringify(key)} was accessed during render ` +
|
`Property ${JSON.stringify(key)} was accessed during render ` +
|
||||||
@ -157,7 +157,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
|||||||
warn(`Attempting to mutate prop "${key}". Props are readonly.`, target)
|
warn(`Attempting to mutate prop "${key}". Props are readonly.`, target)
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
target.user[key] = value
|
target.sink[key] = value
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,7 @@ import {
|
|||||||
queueEffectWithSuspense
|
queueEffectWithSuspense
|
||||||
} from './suspense'
|
} from './suspense'
|
||||||
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
|
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
|
||||||
|
import { KeepAliveSink } from './keepAlive'
|
||||||
|
|
||||||
export interface RendererOptions<HostNode = any, HostElement = any> {
|
export interface RendererOptions<HostNode = any, HostElement = any> {
|
||||||
patchProp(
|
patchProp(
|
||||||
@ -131,7 +132,7 @@ function isSameType(n1: VNode, n2: VNode): boolean {
|
|||||||
return n1.type === n2.type && n1.key === n2.key
|
return n1.type === n2.type && n1.key === n2.key
|
||||||
}
|
}
|
||||||
|
|
||||||
function invokeHooks(hooks: Function[], arg?: DebuggerEvent) {
|
export function invokeHooks(hooks: Function[], arg?: DebuggerEvent) {
|
||||||
for (let i = 0; i < hooks.length; i++) {
|
for (let i = 0; i < hooks.length; i++) {
|
||||||
hooks[i](arg)
|
hooks[i](arg)
|
||||||
}
|
}
|
||||||
@ -755,14 +756,22 @@ export function createRenderer<
|
|||||||
optimized: boolean
|
optimized: boolean
|
||||||
) {
|
) {
|
||||||
if (n1 == null) {
|
if (n1 == null) {
|
||||||
mountComponent(
|
if (n2.shapeFlag & ShapeFlags.STATEFUL_COMPONENT_KEPT_ALIVE) {
|
||||||
n2,
|
;(parentComponent!.sink as KeepAliveSink).activate(
|
||||||
container,
|
n2,
|
||||||
anchor,
|
container,
|
||||||
parentComponent,
|
anchor
|
||||||
parentSuspense,
|
)
|
||||||
isSVG
|
} else {
|
||||||
)
|
mountComponent(
|
||||||
|
n2,
|
||||||
|
container,
|
||||||
|
anchor,
|
||||||
|
parentComponent,
|
||||||
|
parentSuspense,
|
||||||
|
isSVG
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const instance = (n2.component = n1.component)!
|
const instance = (n2.component = n1.component)!
|
||||||
|
|
||||||
@ -816,8 +825,17 @@ export function createRenderer<
|
|||||||
pushWarningContext(initialVNode)
|
pushWarningContext(initialVNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Comp = initialVNode.type as Component
|
||||||
|
|
||||||
|
// inject renderer internals for keepAlive
|
||||||
|
if ((Comp as any).__isKeepAlive) {
|
||||||
|
const sink = instance.sink as KeepAliveSink
|
||||||
|
sink.renderer = internals
|
||||||
|
sink.parentSuspense = parentSuspense
|
||||||
|
}
|
||||||
|
|
||||||
// resolve props and slots for setup context
|
// resolve props and slots for setup context
|
||||||
const propsOptions = (initialVNode.type as Component).props
|
const propsOptions = Comp.props
|
||||||
resolveProps(instance, initialVNode.props, propsOptions)
|
resolveProps(instance, initialVNode.props, propsOptions)
|
||||||
resolveSlots(instance, initialVNode.children)
|
resolveSlots(instance, initialVNode.children)
|
||||||
|
|
||||||
@ -1381,7 +1399,11 @@ export function createRenderer<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shapeFlag & ShapeFlags.COMPONENT) {
|
if (shapeFlag & ShapeFlags.COMPONENT) {
|
||||||
unmountComponent(vnode.component!, parentSuspense, doRemove)
|
if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT_SHOULD_KEEP_ALIVE) {
|
||||||
|
;(parentComponent!.sink as KeepAliveSink).deactivate(vnode)
|
||||||
|
} else {
|
||||||
|
unmountComponent(vnode.component!, parentSuspense, doRemove)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,8 @@ export {
|
|||||||
} from './vnode'
|
} from './vnode'
|
||||||
// VNode type symbols
|
// VNode type symbols
|
||||||
export { Text, Comment, Fragment, Portal, Suspense } from './vnode'
|
export { Text, Comment, Fragment, Portal, Suspense } from './vnode'
|
||||||
|
// Internal Components
|
||||||
|
export { KeepAlive } from './keepAlive'
|
||||||
// VNode flags
|
// VNode flags
|
||||||
export { PublicShapeFlags as ShapeFlags } from './shapeFlags'
|
export { PublicShapeFlags as ShapeFlags } from './shapeFlags'
|
||||||
export { PublicPatchFlags as PatchFlags } from '@vue/shared'
|
export { PublicPatchFlags as PatchFlags } from '@vue/shared'
|
||||||
|
249
packages/runtime-core/src/keepAlive.ts
Normal file
249
packages/runtime-core/src/keepAlive.ts
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
getCurrentInstance,
|
||||||
|
FunctionalComponent,
|
||||||
|
SetupContext,
|
||||||
|
ComponentInternalInstance,
|
||||||
|
LifecycleHooks,
|
||||||
|
currentInstance
|
||||||
|
} from './component'
|
||||||
|
import { VNode, cloneVNode, isVNode } from './vnode'
|
||||||
|
import { warn } from './warning'
|
||||||
|
import { onBeforeUnmount, injectHook } from './apiLifecycle'
|
||||||
|
import { isString, isArray } from '@vue/shared'
|
||||||
|
import { watch } from './apiWatch'
|
||||||
|
import { ShapeFlags } from './shapeFlags'
|
||||||
|
import { SuspenseBoundary } from './suspense'
|
||||||
|
import {
|
||||||
|
RendererInternals,
|
||||||
|
queuePostRenderEffect,
|
||||||
|
invokeHooks
|
||||||
|
} from './createRenderer'
|
||||||
|
|
||||||
|
type MatchPattern = string | RegExp | string[] | RegExp[]
|
||||||
|
|
||||||
|
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: object, anchor: object | null) => void
|
||||||
|
deactivate: (vnode: VNode) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KeepAlive = {
|
||||||
|
name: `KeepAlive`,
|
||||||
|
__isKeepAlive: true,
|
||||||
|
setup(props: KeepAliveProps, { slots }: SetupContext) {
|
||||||
|
const cache: Cache = new Map()
|
||||||
|
const keys: Keys = new Set()
|
||||||
|
let current: VNode | null = null
|
||||||
|
|
||||||
|
const instance = getCurrentInstance()!
|
||||||
|
const sink = instance.sink as KeepAliveSink
|
||||||
|
const {
|
||||||
|
renderer: {
|
||||||
|
move,
|
||||||
|
unmount: _unmount,
|
||||||
|
options: { createElement }
|
||||||
|
},
|
||||||
|
parentSuspense
|
||||||
|
} = sink
|
||||||
|
const storageContainer = createElement('div')
|
||||||
|
|
||||||
|
sink.activate = (vnode, container, anchor) => {
|
||||||
|
move(vnode, container, anchor)
|
||||||
|
queuePostRenderEffect(() => {
|
||||||
|
vnode.component!.isDeactivated = false
|
||||||
|
invokeHooks(vnode.component!.a!)
|
||||||
|
}, parentSuspense)
|
||||||
|
}
|
||||||
|
|
||||||
|
sink.deactivate = (vnode: VNode) => {
|
||||||
|
move(vnode, storageContainer, null)
|
||||||
|
queuePostRenderEffect(() => {
|
||||||
|
invokeHooks(vnode.component!.da!)
|
||||||
|
vnode.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)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
},
|
||||||
|
{ lazy: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
cache.forEach(unmount)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (!slots.default) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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 cached = 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 (cached) {
|
||||||
|
// copy over mounted state
|
||||||
|
vnode.el = cached.el
|
||||||
|
vnode.anchor = cached.anchor
|
||||||
|
vnode.component = cached.component
|
||||||
|
// avoid vnode being mounted as fresh
|
||||||
|
vnode.shapeFlag |= ShapeFlags.STATEFUL_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.STATEFUL_COMPONENT_SHOULD_KEEP_ALIVE
|
||||||
|
|
||||||
|
current = vnode
|
||||||
|
return vnode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (__DEV__) {
|
||||||
|
;(KeepAlive as any).props = {
|
||||||
|
include: [String, RegExp, Array],
|
||||||
|
exclude: [String, RegExp, Array],
|
||||||
|
max: [String, Number]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 registerKeepAliveHook(
|
||||||
|
hook: Function,
|
||||||
|
type: LifecycleHooks,
|
||||||
|
target: ComponentInternalInstance | null = currentInstance
|
||||||
|
) {
|
||||||
|
// When registering an activated/deactivated hook, instead of registering it
|
||||||
|
// on the target instance, we walk up the parent chain and register it on
|
||||||
|
// every ancestor instance that is a keep-alive root. 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
|
||||||
|
while (current.parent) {
|
||||||
|
if (current.parent.type === KeepAlive) {
|
||||||
|
register(hook, type, target, current)
|
||||||
|
}
|
||||||
|
current = current.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function register(
|
||||||
|
hook: Function,
|
||||||
|
type: LifecycleHooks,
|
||||||
|
target: ComponentInternalInstance,
|
||||||
|
keepAliveRoot: ComponentInternalInstance
|
||||||
|
) {
|
||||||
|
const wrappedHook = () => {
|
||||||
|
// 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, keepAliveRoot, true)
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
const hooks = keepAliveRoot[type]!
|
||||||
|
hooks.splice(hooks.indexOf(wrappedHook), 1)
|
||||||
|
}, target)
|
||||||
|
}
|
@ -8,6 +8,8 @@ export const enum ShapeFlags {
|
|||||||
ARRAY_CHILDREN = 1 << 4,
|
ARRAY_CHILDREN = 1 << 4,
|
||||||
SLOTS_CHILDREN = 1 << 5,
|
SLOTS_CHILDREN = 1 << 5,
|
||||||
SUSPENSE = 1 << 6,
|
SUSPENSE = 1 << 6,
|
||||||
|
STATEFUL_COMPONENT_SHOULD_KEEP_ALIVE = 1 << 7,
|
||||||
|
STATEFUL_COMPONENT_KEPT_ALIVE = 1 << 8,
|
||||||
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
|
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,13 +264,14 @@ export function cloneVNode<T, U>(
|
|||||||
appContext: vnode.appContext,
|
appContext: vnode.appContext,
|
||||||
dirs: vnode.dirs,
|
dirs: vnode.dirs,
|
||||||
|
|
||||||
// these should be set to null since they should only be present on
|
// These should technically only be non-null on mounted VNodes. However,
|
||||||
// mounted VNodes. If they are somehow not null, this means we have
|
// they *should* be copied for kept-alive vnodes. So we just always copy
|
||||||
// encountered an already-mounted vnode being used again.
|
// them since them being non-null during a mount doesn't affect the logic as
|
||||||
component: null,
|
// they will simply be overwritten.
|
||||||
suspense: null,
|
component: vnode.component,
|
||||||
el: null,
|
suspense: vnode.suspense,
|
||||||
anchor: null
|
el: vnode.el,
|
||||||
|
anchor: vnode.anchor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user