feat: keep-alive

This commit is contained in:
Evan You 2018-09-26 17:10:34 -04:00
parent 5e5dd7b44c
commit 7c2ec8ace0
7 changed files with 187 additions and 30 deletions

View File

@ -89,7 +89,7 @@ class InternalComponent {
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

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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'

View File

@ -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<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
}

View File

@ -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
}