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 _computedGetters: Record<string, ComputedGetter> | null = null
|
||||||
public _watchHandles: Set<Autorun> | null = null
|
public _watchHandles: Set<Autorun> | null = null
|
||||||
public _mounted: boolean = false
|
public _mounted: boolean = false
|
||||||
public _destroyed: boolean = false
|
public _unmounted: boolean = false
|
||||||
public _events: { [event: string]: Function[] | null } | null = null
|
public _events: { [event: string]: Function[] | null } | null = null
|
||||||
public _updateHandle: Autorun | null = null
|
public _updateHandle: Autorun | null = null
|
||||||
public _queueJob: ((fn: () => void) => void) | null = null
|
public _queueJob: ((fn: () => void) => void) | null = null
|
||||||
|
@ -81,8 +81,11 @@ export function renderInstanceRoot(instance: MountedComponent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function teardownComponentInstance(instance: MountedComponent) {
|
export function teardownComponentInstance(instance: MountedComponent) {
|
||||||
|
if (instance._unmounted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const parentComponent = instance.$parent && instance.$parent._self
|
const parentComponent = instance.$parent && instance.$parent._self
|
||||||
if (parentComponent && !parentComponent._destroyed) {
|
if (parentComponent && !parentComponent._unmounted) {
|
||||||
parentComponent.$children.splice(
|
parentComponent.$children.splice(
|
||||||
parentComponent.$children.indexOf(instance.$proxy),
|
parentComponent.$children.indexOf(instance.$proxy),
|
||||||
1
|
1
|
||||||
@ -114,24 +117,28 @@ export function normalizeComponentRoot(
|
|||||||
vnode = createFragment(vnode)
|
vnode = createFragment(vnode)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const { flags } = vnode
|
const { el, flags } = vnode
|
||||||
if (
|
if (
|
||||||
componentVNode &&
|
componentVNode &&
|
||||||
(flags & VNodeFlags.COMPONENT || flags & VNodeFlags.ELEMENT)
|
(flags & VNodeFlags.COMPONENT || flags & VNodeFlags.ELEMENT)
|
||||||
) {
|
) {
|
||||||
|
const isKeepAlive = (flags & VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE) > 0
|
||||||
if (
|
if (
|
||||||
inheritAttrs !== false &&
|
inheritAttrs !== false &&
|
||||||
attrs !== void 0 &&
|
attrs !== void 0 &&
|
||||||
Object.keys(attrs).length > 0
|
Object.keys(attrs).length > 0
|
||||||
) {
|
) {
|
||||||
vnode = cloneVNode(vnode, attrs)
|
vnode = cloneVNode(vnode, attrs)
|
||||||
} else if (vnode.el) {
|
if (isKeepAlive) {
|
||||||
|
vnode.el = el
|
||||||
|
}
|
||||||
|
} else if (el && !isKeepAlive) {
|
||||||
vnode = cloneVNode(vnode)
|
vnode = cloneVNode(vnode)
|
||||||
}
|
}
|
||||||
if (flags & VNodeFlags.COMPONENT) {
|
if (flags & VNodeFlags.COMPONENT) {
|
||||||
vnode.parentVNode = componentVNode
|
vnode.parentVNode = componentVNode
|
||||||
}
|
}
|
||||||
} else if (vnode.el) {
|
} else if (el) {
|
||||||
vnode = cloneVNode(vnode)
|
vnode = cloneVNode(vnode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
normalizeComponentRoot,
|
normalizeComponentRoot,
|
||||||
shouldUpdateFunctionalComponent
|
shouldUpdateFunctionalComponent
|
||||||
} from './componentUtils'
|
} from './componentUtils'
|
||||||
|
import { KeepAliveSymbol } from './optional/keepAlive'
|
||||||
|
|
||||||
interface NodeOps {
|
interface NodeOps {
|
||||||
createElement: (tag: string, isSVG?: boolean) => any
|
createElement: (tag: string, isSVG?: boolean) => any
|
||||||
@ -271,15 +272,22 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
let el: RenderNode | RenderFragment
|
let el: RenderNode | RenderFragment
|
||||||
const { flags, tag, data, slots } = vnode
|
const { flags, tag, data, slots } = vnode
|
||||||
if (flags & VNodeFlags.COMPONENT_STATEFUL) {
|
if (flags & VNodeFlags.COMPONENT_STATEFUL) {
|
||||||
el = mountComponentInstance(
|
if (flags & VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE) {
|
||||||
vnode,
|
// kept-alive
|
||||||
tag as ComponentClass,
|
el = vnode.el as RenderNode
|
||||||
null,
|
// TODO activated hook
|
||||||
parentComponent,
|
} else {
|
||||||
isSVG,
|
el = mountComponentInstance(
|
||||||
endNode
|
vnode,
|
||||||
)
|
tag as ComponentClass,
|
||||||
|
null,
|
||||||
|
parentComponent,
|
||||||
|
isSVG,
|
||||||
|
endNode
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
debugger
|
||||||
// functional component
|
// functional component
|
||||||
const render = tag as FunctionalComponent
|
const render = tag as FunctionalComponent
|
||||||
const { props, attrs } = resolveProps(data, render.props, render)
|
const { props, attrs } = resolveProps(data, render.props, render)
|
||||||
@ -1098,7 +1106,9 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
}
|
}
|
||||||
} else if (flags & VNodeFlags.COMPONENT) {
|
} else if (flags & VNodeFlags.COMPONENT) {
|
||||||
if (flags & VNodeFlags.COMPONENT_STATEFUL) {
|
if (flags & VNodeFlags.COMPONENT_STATEFUL) {
|
||||||
unmountComponentInstance(children as MountedComponent)
|
if ((flags & VNodeFlags.COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE) === 0) {
|
||||||
|
unmountComponentInstance(children as MountedComponent)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
unmount(children as VNode)
|
unmount(children as VNode)
|
||||||
}
|
}
|
||||||
@ -1161,12 +1171,17 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
isSVG: boolean,
|
isSVG: boolean,
|
||||||
endNode: RenderNode | RenderFragment | null
|
endNode: RenderNode | RenderFragment | null
|
||||||
): RenderNode {
|
): RenderNode {
|
||||||
// a vnode may already have an instance if this is a compat call
|
// a vnode may already have an instance if this is a compat call with
|
||||||
// with new Vue()
|
// new Vue()
|
||||||
const instance =
|
const instance =
|
||||||
(__COMPAT__ && (parentVNode.children as MountedComponent)) ||
|
(__COMPAT__ && (parentVNode.children as MountedComponent)) ||
|
||||||
createComponentInstance(parentVNode, Component, parentComponent)
|
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) {
|
if (instance.beforeMount) {
|
||||||
instance.beforeMount.call(instance.$proxy)
|
instance.beforeMount.call(instance.$proxy)
|
||||||
}
|
}
|
||||||
@ -1177,7 +1192,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
|
|
||||||
instance._updateHandle = autorun(
|
instance._updateHandle = autorun(
|
||||||
() => {
|
() => {
|
||||||
if (instance._destroyed) {
|
if (instance._unmounted) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (instance._mounted) {
|
if (instance._mounted) {
|
||||||
@ -1271,6 +1286,9 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function unmountComponentInstance(instance: MountedComponent) {
|
function unmountComponentInstance(instance: MountedComponent) {
|
||||||
|
if (instance._unmounted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (instance.beforeUnmount) {
|
if (instance.beforeUnmount) {
|
||||||
instance.beforeUnmount.call(instance.$proxy)
|
instance.beforeUnmount.call(instance.$proxy)
|
||||||
}
|
}
|
||||||
@ -1279,7 +1297,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
}
|
}
|
||||||
stop(instance._updateHandle)
|
stop(instance._updateHandle)
|
||||||
teardownComponentInstance(instance)
|
teardownComponentInstance(instance)
|
||||||
instance._destroyed = true
|
instance._unmounted = true
|
||||||
if (instance.unmounted) {
|
if (instance.unmounted) {
|
||||||
instance.unmounted.call(instance.$proxy)
|
instance.unmounted.call(instance.$proxy)
|
||||||
}
|
}
|
||||||
|
@ -5,17 +5,18 @@ export const enum VNodeFlags {
|
|||||||
ELEMENT = ELEMENT_HTML | ELEMENT_SVG,
|
ELEMENT = ELEMENT_HTML | ELEMENT_SVG,
|
||||||
|
|
||||||
COMPONENT_UNKNOWN = 1 << 2,
|
COMPONENT_UNKNOWN = 1 << 2,
|
||||||
COMPONENT_STATEFUL = 1 << 3,
|
COMPONENT_STATEFUL_NORMAL = 1 << 3,
|
||||||
COMPONENT_FUNCTIONAL = 1 << 4,
|
COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE = 1 << 4,
|
||||||
COMPONENT_ASYNC = 1 << 5,
|
COMPONENT_STATEFUL_KEPT_ALIVE = 1 << 5,
|
||||||
COMPONENT = COMPONENT_UNKNOWN |
|
COMPONENT_STATEFUL = COMPONENT_STATEFUL_NORMAL |
|
||||||
COMPONENT_STATEFUL |
|
COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE |
|
||||||
COMPONENT_FUNCTIONAL |
|
COMPONENT_STATEFUL_KEPT_ALIVE,
|
||||||
COMPONENT_ASYNC,
|
COMPONENT_FUNCTIONAL = 1 << 6,
|
||||||
|
COMPONENT = COMPONENT_UNKNOWN | COMPONENT_STATEFUL | COMPONENT_FUNCTIONAL,
|
||||||
|
|
||||||
TEXT = 1 << 6,
|
TEXT = 1 << 7,
|
||||||
FRAGMENT = 1 << 7,
|
FRAGMENT = 1 << 8,
|
||||||
PORTAL = 1 << 8
|
PORTAL = 1 << 9
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum ChildrenFlags {
|
export const enum ChildrenFlags {
|
||||||
|
@ -18,6 +18,7 @@ export { createComponentInstance } from './componentUtils'
|
|||||||
export * from './optional/directive'
|
export * from './optional/directive'
|
||||||
export * from './optional/context'
|
export * from './optional/context'
|
||||||
export * from './optional/asyncComponent'
|
export * from './optional/asyncComponent'
|
||||||
|
export * from './optional/keepAlive'
|
||||||
|
|
||||||
// flags & types
|
// flags & types
|
||||||
export { ComponentType, ComponentClass, FunctionalComponent } from './component'
|
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
|
comp = render
|
||||||
} else {
|
} else {
|
||||||
// object literal stateful
|
// object literal stateful
|
||||||
flags = VNodeFlags.COMPONENT_STATEFUL
|
flags = VNodeFlags.COMPONENT_STATEFUL_NORMAL
|
||||||
comp =
|
comp =
|
||||||
comp._normalized ||
|
comp._normalized ||
|
||||||
(comp._normalized = createComponentClassFromOptions(comp))
|
(comp._normalized = createComponentClassFromOptions(comp))
|
||||||
@ -157,7 +157,7 @@ export function createComponentVNode(
|
|||||||
// TODO warn invalid comp value in dev
|
// TODO warn invalid comp value in dev
|
||||||
}
|
}
|
||||||
if (comp.prototype && comp.prototype.render) {
|
if (comp.prototype && comp.prototype.render) {
|
||||||
flags = VNodeFlags.COMPONENT_STATEFUL
|
flags = VNodeFlags.COMPONENT_STATEFUL_NORMAL
|
||||||
} else {
|
} else {
|
||||||
flags = VNodeFlags.COMPONENT_FUNCTIONAL
|
flags = VNodeFlags.COMPONENT_FUNCTIONAL
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user