public _computedGetters: Record<string, ComputedGetter> | null = null
public _watchHandles: Set<Autorun> | null = null
public _mounted: boolean = false
- public _destroyed: boolean = false
+ public _unmounted: boolean = false
public _events: { [event: string]: Function[] | null } | null = null
public _updateHandle: Autorun | null = null
public _queueJob: ((fn: () => void) => void) | null = null
}
export function teardownComponentInstance(instance: MountedComponent) {
+ if (instance._unmounted) {
+ return
+ }
const parentComponent = instance.$parent && instance.$parent._self
- if (parentComponent && !parentComponent._destroyed) {
+ if (parentComponent && !parentComponent._unmounted) {
parentComponent.$children.splice(
parentComponent.$children.indexOf(instance.$proxy),
1
vnode = createFragment(vnode)
}
} else {
- const { flags } = vnode
+ const { el, flags } = vnode
if (
componentVNode &&
(flags & VNodeFlags.COMPONENT || flags & VNodeFlags.ELEMENT)
) {
+ const isKeepAlive = (flags & VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE) > 0
if (
inheritAttrs !== false &&
attrs !== void 0 &&
Object.keys(attrs).length > 0
) {
vnode = cloneVNode(vnode, attrs)
- } else if (vnode.el) {
+ if (isKeepAlive) {
+ vnode.el = el
+ }
+ } else if (el && !isKeepAlive) {
vnode = cloneVNode(vnode)
}
if (flags & VNodeFlags.COMPONENT) {
vnode.parentVNode = componentVNode
}
- } else if (vnode.el) {
+ } else if (el) {
vnode = cloneVNode(vnode)
}
}
normalizeComponentRoot,
shouldUpdateFunctionalComponent
} from './componentUtils'
+import { KeepAliveSymbol } from './optional/keepAlive'
interface NodeOps {
createElement: (tag: string, isSVG?: boolean) => any
let el: RenderNode | RenderFragment
const { flags, tag, data, slots } = vnode
if (flags & VNodeFlags.COMPONENT_STATEFUL) {
- el = mountComponentInstance(
- vnode,
- tag as ComponentClass,
- null,
- parentComponent,
- isSVG,
- endNode
- )
+ if (flags & VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE) {
+ // kept-alive
+ el = vnode.el as RenderNode
+ // TODO activated hook
+ } else {
+ el = mountComponentInstance(
+ vnode,
+ tag as ComponentClass,
+ null,
+ parentComponent,
+ isSVG,
+ endNode
+ )
+ }
} else {
+ debugger
// functional component
const render = tag as FunctionalComponent
const { props, attrs } = resolveProps(data, render.props, render)
}
} else if (flags & VNodeFlags.COMPONENT) {
if (flags & VNodeFlags.COMPONENT_STATEFUL) {
- unmountComponentInstance(children as MountedComponent)
+ if ((flags & VNodeFlags.COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE) === 0) {
+ unmountComponentInstance(children as MountedComponent)
+ }
} else {
unmount(children as VNode)
}
isSVG: boolean,
endNode: RenderNode | RenderFragment | null
): RenderNode {
- // a vnode may already have an instance if this is a compat call
- // with new Vue()
+ // a vnode may already have an instance if this is a compat call with
+ // new Vue()
const instance =
(__COMPAT__ && (parentVNode.children as MountedComponent)) ||
createComponentInstance(parentVNode, Component, parentComponent)
+ // inject platform-specific unmount to keep-alive container
+ if ((Component as any)[KeepAliveSymbol] === true) {
+ ;(instance as any).$unmount = unmountComponentInstance
+ }
+
if (instance.beforeMount) {
instance.beforeMount.call(instance.$proxy)
}
instance._updateHandle = autorun(
() => {
- if (instance._destroyed) {
+ if (instance._unmounted) {
return
}
if (instance._mounted) {
}
function unmountComponentInstance(instance: MountedComponent) {
+ if (instance._unmounted) {
+ return
+ }
if (instance.beforeUnmount) {
instance.beforeUnmount.call(instance.$proxy)
}
}
stop(instance._updateHandle)
teardownComponentInstance(instance)
- instance._destroyed = true
+ instance._unmounted = true
if (instance.unmounted) {
instance.unmounted.call(instance.$proxy)
}
ELEMENT = ELEMENT_HTML | ELEMENT_SVG,
COMPONENT_UNKNOWN = 1 << 2,
- COMPONENT_STATEFUL = 1 << 3,
- COMPONENT_FUNCTIONAL = 1 << 4,
- COMPONENT_ASYNC = 1 << 5,
- COMPONENT = COMPONENT_UNKNOWN |
- COMPONENT_STATEFUL |
- COMPONENT_FUNCTIONAL |
- COMPONENT_ASYNC,
+ COMPONENT_STATEFUL_NORMAL = 1 << 3,
+ COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE = 1 << 4,
+ COMPONENT_STATEFUL_KEPT_ALIVE = 1 << 5,
+ COMPONENT_STATEFUL = COMPONENT_STATEFUL_NORMAL |
+ COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE |
+ COMPONENT_STATEFUL_KEPT_ALIVE,
+ COMPONENT_FUNCTIONAL = 1 << 6,
+ COMPONENT = COMPONENT_UNKNOWN | COMPONENT_STATEFUL | COMPONENT_FUNCTIONAL,
- TEXT = 1 << 6,
- FRAGMENT = 1 << 7,
- PORTAL = 1 << 8
+ TEXT = 1 << 7,
+ FRAGMENT = 1 << 8,
+ PORTAL = 1 << 9
}
export const enum ChildrenFlags {
export * from './optional/directive'
export * from './optional/context'
export * from './optional/asyncComponent'
+export * from './optional/keepAlive'
// flags & types
export { ComponentType, ComponentClass, FunctionalComponent } from './component'
+import { Component, ComponentClass, MountedComponent } from '../component'
+import { VNode, Slots } from '../vdom'
+import { VNodeFlags } from '../flags'
+
+type MatchPattern = string | RegExp | string[] | RegExp[]
+
+interface KeepAliveProps {
+ include?: MatchPattern
+ exclude?: MatchPattern
+ max?: number | string
+}
+
+type CacheKey = string | number | ComponentClass
+type Cache = Map<CacheKey, VNode>
+
+export const KeepAliveSymbol = Symbol()
+
+export class KeepAlive extends Component<{}, KeepAliveProps> {
+ cache: Cache
+ keys: Set<CacheKey>
+
+ // to be set in createRenderer when instance is created
+ $unmount: (instance: MountedComponent) => void
+
+ created() {
+ this.cache = new Map()
+ // keys represents the "freshness" of cached components
+ // oldest cached ones will be pruned first when cache count exceeds max
+ this.keys = new Set()
+ }
+
+ unmounted() {
+ this.cache.forEach(vnode => {
+ // change flag so it can be properly unmounted
+ vnode.flags = VNodeFlags.COMPONENT_STATEFUL_NORMAL
+ this.$unmount(vnode.children as MountedComponent)
+ })
+ }
+
+ pruneCache(filter?: (name: string) => boolean) {
+ this.cache.forEach((vnode, key) => {
+ const name = getName(vnode.tag as ComponentClass)
+ if (name && (!filter || !filter(name))) {
+ this.pruneCacheEntry(key)
+ }
+ })
+ }
+
+ pruneCacheEntry(key: CacheKey) {
+ const cached = this.cache.get(key) as VNode
+ const current = this.$vnode
+ if (!current || cached.tag !== current.tag) {
+ this.$unmount(cached.children as MountedComponent)
+ }
+ this.cache.delete(key)
+ this.keys.delete(key)
+ }
+
+ render(props: any, slots: Slots) {
+ if (!slots.default) {
+ return
+ }
+ const children = slots.default()
+ let vnode = children[0]
+ if (children.length > 1) {
+ if (__DEV__) {
+ console.warn(`KeepAlive can only have a single child.`)
+ }
+ return children
+ } else if ((vnode.flags & VNodeFlags.COMPONENT_STATEFUL) === 0) {
+ if (__DEV__) {
+ console.warn(`KeepAlive child must be a stateful component.`)
+ }
+ return children
+ }
+
+ const comp = vnode.tag as ComponentClass
+ const name = getName(comp)
+ const { include, exclude, max } = props
+
+ if (
+ (include && (!name || !matches(include, name))) ||
+ (exclude && name && matches(exclude, name))
+ ) {
+ return vnode
+ }
+
+ const { cache, keys } = this
+ const key = vnode.key == null ? comp : vnode.key
+ const cached = cache.get(key)
+ if (cached) {
+ vnode.children = cached.children
+ vnode.el = cached.el
+ vnode.flags |= VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE
+ // make this key the freshest
+ keys.delete(key)
+ keys.add(key)
+ } else {
+ cache.set(key, vnode)
+ keys.add(key)
+ // prune oldest entry
+ if (max && keys.size > parseInt(max, 10)) {
+ this.pruneCacheEntry(Array.from(this.keys)[0])
+ }
+ }
+ vnode.flags |= VNodeFlags.COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE
+ return vnode
+ }
+}
+
+// mark constructor
+// we use a symbol instead of comparing to the constructor itself
+// so that the implementation can be tree-shaken
+;(KeepAlive as any)[KeepAliveSymbol] = true
+
+function getName(comp: ComponentClass): string | void {
+ return comp.options && comp.options.name
+}
+
+function matches(pattern: MatchPattern, name: string): boolean {
+ if (Array.isArray(pattern)) {
+ return (pattern as any).some((p: string | RegExp) => matches(p, name))
+ } else if (typeof pattern === 'string') {
+ return pattern.split(',').indexOf(name) > -1
+ } else if (pattern.test) {
+ return pattern.test(name)
+ }
+ /* istanbul ignore next */
+ return false
+}
comp = render
} else {
// object literal stateful
- flags = VNodeFlags.COMPONENT_STATEFUL
+ flags = VNodeFlags.COMPONENT_STATEFUL_NORMAL
comp =
comp._normalized ||
(comp._normalized = createComponentClassFromOptions(comp))
// TODO warn invalid comp value in dev
}
if (comp.prototype && comp.prototype.render) {
- flags = VNodeFlags.COMPONENT_STATEFUL
+ flags = VNodeFlags.COMPONENT_STATEFUL_NORMAL
} else {
flags = VNodeFlags.COMPONENT_FUNCTIONAL
}