feat: keep-alive
This commit is contained in:
parent
5e5dd7b44c
commit
7c2ec8ace0
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
normalizeComponentRoot,
|
||||
shouldUpdateFunctionalComponent
|
||||
} from './componentUtils'
|
||||
import { KeepAliveSymbol } from './optional/keepAlive'
|
||||
|
||||
interface NodeOps {
|
||||
createElement: (tag: string, isSVG?: boolean) => any
|
||||
@ -271,6 +272,11 @@ export function createRenderer(options: RendererOptions) {
|
||||
let el: RenderNode | RenderFragment
|
||||
const { flags, tag, data, slots } = vnode
|
||||
if (flags & VNodeFlags.COMPONENT_STATEFUL) {
|
||||
if (flags & VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE) {
|
||||
// kept-alive
|
||||
el = vnode.el as RenderNode
|
||||
// TODO activated hook
|
||||
} else {
|
||||
el = mountComponentInstance(
|
||||
vnode,
|
||||
tag as ComponentClass,
|
||||
@ -279,7 +285,9 @@ export function createRenderer(options: RendererOptions) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user