expect(`Attempting to mutate public property "$data"`).toHaveBeenWarned()
})
- it('user', async () => {
+ it('sink', async () => {
const app = createApp()
let instance: ComponentInternalInstance
let instanceProxy: any
app.mount(Comp, nodeOps.createElement('div'))
instanceProxy.foo = 1
expect(instanceProxy.foo).toBe(1)
- expect(instance!.user.foo).toBe(1)
+ expect(instance!.sink.foo).toBe(1)
})
})
mounted.el = {}
const normalized = normalizeVNode(mounted)
expect(normalized).not.toBe(mounted)
- expect(normalized).toEqual({ ...mounted, el: null })
+ expect(normalized).toEqual(mounted)
// primitive types
expect(normalizeVNode('foo')).toMatchObject({ type: Text, children: `foo` })
expect(cloned2).toEqual(node2)
expect(cloneVNode(node2)).toEqual(node2)
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', () => {
import { warn } from './warning'
import { capitalize } from '@vue/shared'
import { pauseTracking, resumeTracking, DebuggerEvent } from '@vue/reactivity'
+import { registerKeepAliveHook } from './keepAlive'
-function injectHook(
+export function injectHook(
type: LifecycleHooks,
hook: Function,
- target: ComponentInternalInstance | null
+ target: ComponentInternalInstance | null = currentInstance,
+ prepend: boolean = false
) {
if (target) {
- ;(target[type] || (target[type] = [])).push((...args: unknown[]) => {
+ const hooks = target[type] || (target[type] = [])
+ const wrappedHook = (...args: unknown[]) => {
if (target.isUnmounted) {
return
}
setCurrentInstance(null)
resumeTracking()
return res
- })
+ }
+ if (prepend) {
+ hooks.unshift(wrappedHook)
+ } else {
+ hooks.push(wrappedHook)
+ }
} else if (__DEV__) {
const apiName = `on${capitalize(
ErrorTypeStrings[type].replace(/ hook$/, '')
}
}
-const createHook = <T extends Function = () => any>(
+export const createHook = <T extends Function = () => any>(
lifecycle: LifecycleHooks
) => (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
injectHook(lifecycle, hook, target)
export const onErrorCaptured = createHook<ErrorCapturedHook>(
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)
+}
onRenderTracked,
onBeforeUnmount,
onUnmounted,
+ onActivated,
+ onDeactivated,
onRenderTriggered,
DebuggerHook,
ErrorCapturedHook
mounted,
beforeUpdate,
updated,
- // TODO activated
- // TODO deactivated
+ activated,
+ deactivated,
beforeUnmount,
unmounted,
renderTracked,
if (updated) {
onUpdated(updated.bind(ctx))
}
+ if (activated) {
+ onActivated(activated.bind(ctx))
+ }
+ if (deactivated) {
+ onDeactivated(deactivated.bind(ctx))
+ }
if (errorCaptured) {
onErrorCaptured(errorCaptured.bind(ctx))
}
}
export type Component = ComponentOptions | FunctionalComponent
+export { ComponentOptions }
type LifecycleHook = Function[] | null
// after initialized (e.g. inline handlers)
renderCache: (Function | VNode)[] | null
+ // assets for fast resolution
components: Record<string, Component>
directives: Record<string, Directive>
- asyncDep: Promise<any> | null
- asyncResult: unknown
- asyncResolved: boolean
-
// the rest are only for stateful components
renderContext: Data
data: Data
refs: Data
emit: Emit
- // user namespace
- user: { [key: string]: any }
+ // suspense related
+ asyncDep: Promise<any> | null
+ asyncResult: unknown
+ asyncResolved: boolean
+
+ // storage for any extra properties
+ sink: { [key: string]: any }
// lifecycle
isUnmounted: boolean
+ isDeactivated: boolean
[LifecycleHooks.BEFORE_CREATE]: LifecycleHook
[LifecycleHooks.CREATED]: LifecycleHook
[LifecycleHooks.BEFORE_MOUNT]: LifecycleHook
asyncResolved: false,
// 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
// not using enums here because it results in computed properties
isUnmounted: false,
+ isDeactivated: false,
bc: null,
c: null,
bm: null,
propsProxy,
accessCache,
type,
- user
+ sink
} = target
// fast path for unscopables when using `with` block
if (__RUNTIME_COMPILE__ && (key as any) === Symbol.unscopables) {
return instanceWatch.bind(target)
}
}
- if (hasOwn(user, key)) {
- return user[key]
+ if (hasOwn(sink, key)) {
+ return sink[key]
} else if (__DEV__ && currentRenderingInstance != null) {
warn(
`Property ${JSON.stringify(key)} was accessed during render ` +
warn(`Attempting to mutate prop "${key}". Props are readonly.`, target)
return false
} else {
- target.user[key] = value
+ target.sink[key] = value
}
return true
}
queueEffectWithSuspense
} from './suspense'
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
+import { KeepAliveSink } from './keepAlive'
export interface RendererOptions<HostNode = any, HostElement = any> {
patchProp(
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++) {
hooks[i](arg)
}
optimized: boolean
) {
if (n1 == null) {
- mountComponent(
- n2,
- container,
- anchor,
- parentComponent,
- parentSuspense,
- isSVG
- )
+ if (n2.shapeFlag & ShapeFlags.STATEFUL_COMPONENT_KEPT_ALIVE) {
+ ;(parentComponent!.sink as KeepAliveSink).activate(
+ n2,
+ container,
+ anchor
+ )
+ } else {
+ mountComponent(
+ n2,
+ container,
+ anchor,
+ parentComponent,
+ parentSuspense,
+ isSVG
+ )
+ }
} else {
const instance = (n2.component = n1.component)!
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
- const propsOptions = (initialVNode.type as Component).props
+ const propsOptions = Comp.props
resolveProps(instance, initialVNode.props, propsOptions)
resolveSlots(instance, initialVNode.children)
}
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
}
} from './vnode'
// VNode type symbols
export { Text, Comment, Fragment, Portal, Suspense } from './vnode'
+// Internal Components
+export { KeepAlive } from './keepAlive'
// VNode flags
export { PublicShapeFlags as ShapeFlags } from './shapeFlags'
export { PublicPatchFlags as PatchFlags } from '@vue/shared'
--- /dev/null
+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)
+}
ARRAY_CHILDREN = 1 << 4,
SLOTS_CHILDREN = 1 << 5,
SUSPENSE = 1 << 6,
+ STATEFUL_COMPONENT_SHOULD_KEEP_ALIVE = 1 << 7,
+ STATEFUL_COMPONENT_KEPT_ALIVE = 1 << 8,
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
appContext: vnode.appContext,
dirs: vnode.dirs,
- // these should be set to null since they should only be present on
- // mounted VNodes. If they are somehow not null, this means we have
- // encountered an already-mounted vnode being used again.
- component: null,
- suspense: null,
- el: null,
- anchor: null
+ // These should technically only be non-null on mounted VNodes. However,
+ // they *should* be copied for kept-alive vnodes. So we just always copy
+ // them since them being non-null during a mount doesn't affect the logic as
+ // they will simply be overwritten.
+ component: vnode.component,
+ suspense: vnode.suspense,
+ el: vnode.el,
+ anchor: vnode.anchor
}
}