diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index e1667b10..2995956f 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -89,7 +89,7 @@ class InternalComponent { public _computedGetters: Record | null = null public _watchHandles: Set | 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 diff --git a/packages/core/src/componentUtils.ts b/packages/core/src/componentUtils.ts index e8535a28..1630e8ae 100644 --- a/packages/core/src/componentUtils.ts +++ b/packages/core/src/componentUtils.ts @@ -81,8 +81,11 @@ export function renderInstanceRoot(instance: MountedComponent) { } 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 @@ -114,24 +117,28 @@ export function normalizeComponentRoot( 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) } } diff --git a/packages/core/src/createRenderer.ts b/packages/core/src/createRenderer.ts index 6e8a09a8..a33f5f44 100644 --- a/packages/core/src/createRenderer.ts +++ b/packages/core/src/createRenderer.ts @@ -26,6 +26,7 @@ import { normalizeComponentRoot, shouldUpdateFunctionalComponent } from './componentUtils' +import { KeepAliveSymbol } from './optional/keepAlive' interface NodeOps { createElement: (tag: string, isSVG?: boolean) => any @@ -271,15 +272,22 @@ export function createRenderer(options: RendererOptions) { 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) @@ -1098,7 +1106,9 @@ export function createRenderer(options: RendererOptions) { } } 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) } @@ -1161,12 +1171,17 @@ export function createRenderer(options: RendererOptions) { 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) } @@ -1177,7 +1192,7 @@ export function createRenderer(options: RendererOptions) { instance._updateHandle = autorun( () => { - if (instance._destroyed) { + if (instance._unmounted) { return } if (instance._mounted) { @@ -1271,6 +1286,9 @@ export function createRenderer(options: RendererOptions) { } function unmountComponentInstance(instance: MountedComponent) { + if (instance._unmounted) { + return + } if (instance.beforeUnmount) { instance.beforeUnmount.call(instance.$proxy) } @@ -1279,7 +1297,7 @@ export function createRenderer(options: RendererOptions) { } stop(instance._updateHandle) teardownComponentInstance(instance) - instance._destroyed = true + instance._unmounted = true if (instance.unmounted) { instance.unmounted.call(instance.$proxy) } diff --git a/packages/core/src/flags.ts b/packages/core/src/flags.ts index c248c4e5..3322e09f 100644 --- a/packages/core/src/flags.ts +++ b/packages/core/src/flags.ts @@ -5,17 +5,18 @@ export const enum VNodeFlags { 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 { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fdafe97c..aeb14ca5 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -18,6 +18,7 @@ export { createComponentInstance } from './componentUtils' export * from './optional/directive' export * from './optional/context' export * from './optional/asyncComponent' +export * from './optional/keepAlive' // flags & types export { ComponentType, ComponentClass, FunctionalComponent } from './component' diff --git a/packages/core/src/optional/keepAlive.ts b/packages/core/src/optional/keepAlive.ts index e69de29b..cb045cfc 100644 --- a/packages/core/src/optional/keepAlive.ts +++ b/packages/core/src/optional/keepAlive.ts @@ -0,0 +1,130 @@ +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 + +export const KeepAliveSymbol = Symbol() + +export class KeepAlive extends Component<{}, KeepAliveProps> { + cache: Cache + keys: Set + + // 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 +} diff --git a/packages/core/src/vdom.ts b/packages/core/src/vdom.ts index 578cff95..c586dc4e 100644 --- a/packages/core/src/vdom.ts +++ b/packages/core/src/vdom.ts @@ -146,7 +146,7 @@ export function createComponentVNode( comp = render } else { // object literal stateful - flags = VNodeFlags.COMPONENT_STATEFUL + flags = VNodeFlags.COMPONENT_STATEFUL_NORMAL comp = comp._normalized || (comp._normalized = createComponentClassFromOptions(comp)) @@ -157,7 +157,7 @@ export function createComponentVNode( // 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 }