import {
- type ComponentInternalInstance,
type ComponentOptions,
type ConcreteComponent,
type GenericComponentInstance,
type SetupContext,
getComponentName,
+ getCurrentGenericInstance,
getCurrentInstance,
} from '../component'
import {
}
}
-function matches(pattern: MatchPattern, name: string): boolean {
+export function matches(pattern: MatchPattern, name: string): boolean {
if (isArray(pattern)) {
return pattern.some((p: string | RegExp) => matches(p, name))
} else if (isString(pattern)) {
export function onActivated(
hook: Function,
- target?: ComponentInternalInstance | null,
+ target?: GenericComponentInstance | null,
): void {
registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target)
}
export function onDeactivated(
hook: Function,
- target?: ComponentInternalInstance | null,
+ target?: GenericComponentInstance | null,
): void {
registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target)
}
function registerKeepAliveHook(
hook: Function & { __wdc?: Function },
type: LifecycleHooks,
- target: ComponentInternalInstance | null = getCurrentInstance(),
+ target: GenericComponentInstance | null = getCurrentGenericInstance(),
) {
// cache the deactivate branch check wrapper for injected hooks so the same
// hook can be properly deduped by the scheduler. "__wdc" stands for "with
// arrays.
if (target) {
let current = target.parent
- while (current && current.parent && current.parent.vnode) {
- if (isKeepAlive(current.parent.vnode)) {
+ while (current && current.parent) {
+ let parent = current.parent
+ if (isKeepAlive(parent.vapor ? (parent as any) : current.parent.vnode)) {
injectToKeepAliveRoot(wrappedHook, type, target, current)
}
current = current.parent
function injectToKeepAliveRoot(
hook: Function & { __weh?: Function },
type: LifecycleHooks,
- target: ComponentInternalInstance,
+ target: GenericComponentInstance,
keepAliveRoot: GenericComponentInstance,
) {
// injectHook wraps the original for error handling, so make sure to remove
/**
* @internal
*/
-export { type RendererInternals, MoveType } from './renderer'
+export { type RendererInternals, MoveType, invalidateMount } from './renderer'
/**
* @internal
*/
* @internal
*/
export { initFeatureFlags } from './featureFlags'
+/**
+ * @internal
+ */
+export { getComponentName } from './component'
+/**
+ * @internal
+ */
+export { matches, isKeepAlive } from './components/KeepAlive'
+/**
+ * @internal
+ */
+export { devtoolsComponentAdded } from './devtools'
--- /dev/null
+import { type VaporComponent, createComponent } from '../../src/component'
+import { makeRender } from '../_utils'
+import { VaporKeepAlive } from '../../src/components/KeepAlive'
+import { defineVaporComponent } from '../../src/apiDefineComponent'
+import { child } from '../../src/dom/node'
+import { setText } from '../../src/dom/prop'
+import { template } from '../../src/dom/template'
+import { renderEffect } from '../../src/renderEffect'
+import { createTemplateRefSetter } from '../../src/apiTemplateRef'
+import { createDynamicComponent } from '../../src/apiCreateDynamicComponent'
+import {
+ nextTick,
+ onActivated,
+ onBeforeMount,
+ onDeactivated,
+ onMounted,
+ onUnmounted,
+ ref,
+} from 'vue'
+
+const define = makeRender()
+
+describe('VaporKeepAlive', () => {
+ let one: VaporComponent
+ let two: VaporComponent
+ let oneTest: VaporComponent
+ let views: Record<string, VaporComponent>
+ let root: HTMLDivElement
+
+ beforeEach(() => {
+ root = document.createElement('div')
+ one = defineVaporComponent({
+ name: 'one',
+ setup(_, { expose }) {
+ onBeforeMount(vi.fn())
+ onMounted(vi.fn())
+ onActivated(vi.fn())
+ onDeactivated(vi.fn())
+ onUnmounted(vi.fn())
+
+ const msg = ref('one')
+ expose({ setMsg: (m: string) => (msg.value = m) })
+
+ const n0 = template(`<div> </div>`)() as any
+ const x0 = child(n0) as any
+ renderEffect(() => setText(x0, msg.value))
+ return n0
+ },
+ })
+ oneTest = defineVaporComponent({
+ name: 'oneTest',
+ setup() {
+ onBeforeMount(vi.fn())
+ onMounted(vi.fn())
+ onActivated(vi.fn())
+ onDeactivated(vi.fn())
+ onUnmounted(vi.fn())
+
+ const msg = ref('oneTest')
+ const n0 = template(`<div> </div>`)() as any
+ const x0 = child(n0) as any
+ renderEffect(() => setText(x0, msg.value))
+ return n0
+ },
+ })
+ two = defineVaporComponent({
+ name: 'two',
+ setup() {
+ onBeforeMount(vi.fn())
+ onMounted(vi.fn())
+ onActivated(vi.fn())
+ onDeactivated(vi.fn())
+ onUnmounted(vi.fn())
+
+ const msg = ref('two')
+ const n0 = template(`<div> </div>`)() as any
+ const x0 = child(n0) as any
+ renderEffect(() => setText(x0, msg.value))
+ return n0
+ },
+ })
+ views = {
+ one,
+ oneTest,
+ two,
+ }
+ })
+
+ test('should preserve state', async () => {
+ const viewRef = ref('one')
+ const instanceRef = ref<any>(null)
+
+ const { mount } = define({
+ setup() {
+ const setTemplateRef = createTemplateRefSetter()
+ const n4 = createComponent(VaporKeepAlive as any, null, {
+ default: () => {
+ const n0 = createDynamicComponent(() => views[viewRef.value]) as any
+ setTemplateRef(n0, instanceRef)
+ return n0
+ },
+ })
+ return n4
+ },
+ }).create()
+
+ mount(root)
+ expect(root.innerHTML).toBe(`<div>one</div><!--dynamic-component-->`)
+
+ instanceRef.value.setMsg('changed')
+ await nextTick()
+ expect(root.innerHTML).toBe(`<div>changed</div><!--dynamic-component-->`)
+
+ viewRef.value = 'two'
+ await nextTick()
+ expect(root.innerHTML).toBe(`<div>two</div><!--dynamic-component-->`)
+
+ viewRef.value = 'one'
+ await nextTick()
+ expect(root.innerHTML).toBe(`<div>changed</div><!--dynamic-component-->`)
+ })
+})
isString,
remove,
} from '@vue/shared'
+import { DynamicFragment } from './block'
export type NodeRef = string | Ref | ((ref: Element) => void)
export type RefEl = Element | VaporComponentInstance
if (!instance || instance.isUnmounted) return
const setupState: any = __DEV__ ? instance.setupState || {} : null
- const refValue = isVaporComponent(el) ? getExposed(el) || el : el
-
+ const refValue = isVaporComponent(el)
+ ? getExposed(el) || el
+ : el instanceof DynamicFragment
+ ? getExposed(el.nodes as VaporComponentInstance) || el.nodes
+ : el
const refs =
instance.refs === EMPTY_OBJ ? (instance.refs = {}) : instance.refs
currentInstance,
endMeasure,
expose,
+ isKeepAlive,
nextUid,
popWarningContext,
pushWarningContext,
import { hmrReload, hmrRerender } from './hmr'
import { isHydrating, locateHydrationNode } from './dom/hydration'
import { insertionAnchor, insertionParent } from './insertionState'
+import type { KeepAliveInstance } from './components/KeepAlive'
export { currentInstance } from '@vue/runtime-dom'
onScopeDispose(() => unmountComponent(instance), true)
if (!isHydrating && _insertionParent) {
- insert(instance.block, _insertionParent, _insertionAnchor)
+ mountComponent(instance, _insertionParent, _insertionAnchor)
}
return instance
export function mountComponent(
instance: VaporComponentInstance,
- parent: ParentNode,
+ parentNode: ParentNode,
anchor?: Node | null | 0,
): void {
+ let parent
+ if (
+ (parent = instance.parent) &&
+ isKeepAlive(parent as any) &&
+ (parent as KeepAliveInstance).isKeptAlive(instance)
+ ) {
+ ;(parent as KeepAliveInstance).activate(instance, parentNode, anchor as any)
+ return
+ }
+
if (__DEV__) {
startMeasure(instance, `mount`)
}
if (instance.bm) invokeArrayFns(instance.bm)
- insert(instance.block, parent, anchor)
+ insert(instance.block, parentNode, anchor)
if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!))
instance.isMounted = true
if (__DEV__) {
instance: VaporComponentInstance,
parentNode?: ParentNode,
): void {
+ let parent
+ if (
+ (parent = instance.parent) &&
+ isKeepAlive(parent as any) &&
+ (parent as KeepAliveInstance).shouldKeepAlive(instance)
+ ) {
+ ;(parent as KeepAliveInstance).deactivate(instance)
+ return
+ }
+
if (instance.isMounted && !instance.isUnmounted) {
if (__DEV__ && instance.type.__hmrId) {
unregisterHMR(instance)
--- /dev/null
+import {
+ type KeepAliveProps,
+ currentInstance,
+ devtoolsComponentAdded,
+ getComponentName,
+ invalidateMount,
+ matches,
+ onBeforeUnmount,
+ onMounted,
+ onUpdated,
+ queuePostFlushCb,
+ warn,
+ watch,
+} from '@vue/runtime-dom'
+import { type Block, insert, isFragment, isValidBlock, remove } from '../block'
+import {
+ type VaporComponent,
+ type VaporComponentInstance,
+ isVaporComponent,
+} from '../component'
+import { defineVaporComponent } from '../apiDefineComponent'
+import { invokeArrayFns, isArray } from '@vue/shared'
+
+export interface KeepAliveInstance extends VaporComponentInstance {
+ activate: (
+ instance: VaporComponentInstance,
+ parentNode: ParentNode,
+ anchor: Node,
+ ) => void
+ deactivate: (instance: VaporComponentInstance) => void
+ shouldKeepAlive: (instance: VaporComponentInstance) => boolean
+ isKeptAlive: (instance: VaporComponentInstance) => boolean
+}
+
+type CacheKey = PropertyKey | VaporComponent
+type Cache = Map<CacheKey, VaporComponentInstance>
+type Keys = Set<CacheKey>
+
+const VaporKeepAliveImpl = defineVaporComponent({
+ name: 'VaporKeepAlive',
+ // @ts-expect-error
+ __isKeepAlive: true,
+ props: {
+ include: [String, RegExp, Array],
+ exclude: [String, RegExp, Array],
+ max: [String, Number],
+ },
+ setup(props: KeepAliveProps, { slots }) {
+ if (!slots.default) {
+ return undefined
+ }
+
+ const keepAliveInstance = currentInstance! as KeepAliveInstance
+ const cache: Cache = new Map()
+ const keys: Keys = new Set()
+ const storageContainer = document.createElement('div')
+
+ if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
+ ;(keepAliveInstance as any).__v_cache = cache
+ }
+
+ const { include, exclude, max } = props
+
+ function cacheBlock() {
+ // TODO suspense
+ const current = keepAliveInstance.block!
+ if (!isValidBlock(current)) return
+
+ const block = getInnerBlock(current)!
+ if (!block) return
+
+ const key = block.type
+ if (cache.has(key)) {
+ // make this key the freshest
+ keys.delete(key)
+ keys.add(key)
+ } else {
+ keys.add(key)
+ // prune oldest entry
+ if (max && keys.size > parseInt(max as string, 10)) {
+ pruneCacheEntry(keys.values().next().value!)
+ }
+ }
+ cache.set(key, block)
+ }
+
+ onMounted(cacheBlock)
+ onUpdated(cacheBlock)
+ onBeforeUnmount(() => cache.forEach(cached => remove(cached)))
+
+ const children = slots.default()
+ if (isArray(children) && children.length > 1) {
+ if (__DEV__) {
+ warn(`KeepAlive should contain exactly one component child.`)
+ }
+ return children
+ }
+
+ keepAliveInstance.activate = (
+ instance: VaporComponentInstance,
+ parentNode: ParentNode,
+ anchor: Node,
+ ) => {
+ invalidateMount(instance.m)
+ invalidateMount(instance.a)
+
+ const cachedBlock = cache.get(instance.type)!
+ insert((instance.block = cachedBlock.block), parentNode, anchor)
+ queuePostFlushCb(() => {
+ instance.isDeactivated = false
+ if (instance.a) invokeArrayFns(instance.a)
+ })
+
+ if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
+ devtoolsComponentAdded(instance)
+ }
+ }
+
+ keepAliveInstance.deactivate = (instance: VaporComponentInstance) => {
+ insert(instance.block, storageContainer)
+ queuePostFlushCb(() => {
+ if (instance.da) invokeArrayFns(instance.da)
+ instance.isDeactivated = true
+ })
+
+ if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
+ devtoolsComponentAdded(instance)
+ }
+ }
+
+ keepAliveInstance.shouldKeepAlive = (instance: VaporComponentInstance) => {
+ const name = getComponentName(instance.type)
+ if (
+ (include && (!name || !matches(include, name))) ||
+ (exclude && name && matches(exclude, name))
+ ) {
+ return false
+ }
+ return true
+ }
+
+ keepAliveInstance.isKeptAlive = (instance: VaporComponentInstance) => {
+ return cache.has(instance.type)
+ }
+
+ function pruneCache(filter: (name: string) => boolean) {
+ cache.forEach((instance, key) => {
+ const name = getComponentName(instance.type)
+ if (name && !filter(name)) {
+ pruneCacheEntry(key)
+ }
+ })
+ }
+
+ function pruneCacheEntry(key: CacheKey) {
+ const cached = cache.get(key)
+ if (cached) {
+ remove(cached)
+ }
+ cache.delete(key)
+ keys.delete(key)
+ }
+
+ // prune cache on include/exclude prop change
+ watch(
+ () => [props.include, props.exclude],
+ ([include, exclude]) => {
+ include && pruneCache(name => matches(include, name))
+ exclude && pruneCache(name => !matches(exclude, name))
+ },
+ // prune post-render after `current` has been updated
+ { flush: 'post', deep: true },
+ )
+
+ return children
+ },
+})
+
+export const VaporKeepAlive = VaporKeepAliveImpl as any as {
+ __isKeepAlive: true
+ new (): {
+ $props: KeepAliveProps
+ $slots: {
+ default(): Block
+ }
+ }
+}
+
+function getInnerBlock(block: Block): VaporComponentInstance | undefined {
+ if (isVaporComponent(block)) {
+ return block
+ }
+ if (isFragment(block)) {
+ return getInnerBlock(block.nodes)
+ }
+}
export { defineVaporComponent } from './apiDefineComponent'
export { vaporInteropPlugin } from './vdomInterop'
export type { VaporDirective } from './directives/custom'
+export { VaporKeepAlive } from './components/KeepAlive'
// compiler-use only
export { insert, prepend, remove, isFragment, VaporFragment } from './block'