Skip to content

Commit 4c28469

Browse files
committed
feat(runtime-vapor): KeepAlive (wip)
1 parent cf642a7 commit 4c28469

File tree

2 files changed

+231
-0
lines changed

2 files changed

+231
-0
lines changed

packages/runtime-vapor/src/component.ts

+1
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ export interface ComponentInternalInstance {
187187
isMounted: boolean
188188
isUnmounted: boolean
189189
isUpdating: boolean
190+
isDeactivated?: boolean
190191
// TODO: registory of provides, lifecycles, ...
191192
/**
192193
* @internal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import {
2+
type Component,
3+
type ComponentInternalInstance,
4+
type ObjectComponent,
5+
getCurrentInstance,
6+
} from '../component'
7+
import type { Block } from '../apiRender'
8+
import { invokeArrayFns, isArray, isRegExp, isString } from '@vue/shared'
9+
import { queuePostFlushCb } from '../scheduler'
10+
import { watch } from '../apiWatch'
11+
import { onBeforeUnmount, onMounted, onUpdated } from '../apiLifecycle'
12+
import { warn } from '..'
13+
14+
type MatchPattern = string | RegExp | (string | RegExp)[]
15+
16+
export interface KeepAliveProps {
17+
include?: MatchPattern
18+
exclude?: MatchPattern
19+
max?: number | string
20+
}
21+
22+
type CacheKey = PropertyKey | Component
23+
type Cache = Map<CacheKey, ComponentInternalInstance>
24+
type Keys = Set<CacheKey>
25+
26+
// TODO: render coantext alternative
27+
export interface KeepAliveComponentInternalInstance
28+
extends ComponentInternalInstance {
29+
activate: () => void
30+
deactivate: () => void
31+
}
32+
33+
export const isKeepAlive = (instance: ComponentInternalInstance): boolean =>
34+
(instance as any).__isKeepAlive
35+
36+
const KeepAliveImpl: ObjectComponent = {
37+
name: 'KeepAlive',
38+
39+
// @ts-expect-error
40+
__isKeepAlive: true,
41+
42+
props: {
43+
include: [String, RegExp, Array],
44+
exclude: [String, RegExp, Array],
45+
max: [String, Number],
46+
},
47+
48+
setup(props: KeepAliveProps, { slots }) {
49+
const instance = getCurrentInstance() as KeepAliveComponentInternalInstance
50+
51+
// TODO: ssr
52+
53+
const cache: Cache = new Map()
54+
const keys: Keys = new Set()
55+
let current: ComponentInternalInstance | null = null
56+
57+
// TODO: is it necessary?
58+
// if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
59+
// ;(instance as any).__v_cache = cache
60+
// }
61+
62+
// TODO: suspense
63+
// const parentSuspense = instance.suspense
64+
65+
// TODO:
66+
// const storageContainer = template('<div></div>')()
67+
68+
instance.activate = () => {
69+
// TODO:
70+
// move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
71+
72+
// TODO: suspense (queuePostRenderEffect)
73+
queuePostFlushCb(() => {
74+
instance.isDeactivated = false
75+
if (instance.a) {
76+
invokeArrayFns(instance.a)
77+
}
78+
})
79+
80+
// TODO: devtools
81+
// if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
82+
// // Update components tree
83+
// devtoolsComponentAdded(instance)
84+
// }
85+
}
86+
87+
instance.deactivate = () => {
88+
// TODO:
89+
// invalidateMount(instance.m)
90+
// invalidateMount(instance.a)
91+
// move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
92+
93+
// TODO: suspense (queuePostRenderEffect)
94+
queuePostFlushCb(() => {
95+
if (instance.da) {
96+
invokeArrayFns(instance.da)
97+
}
98+
instance.isDeactivated = true
99+
})
100+
101+
// TODO: devtools
102+
// if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
103+
// // Update components tree
104+
// devtoolsComponentAdded(instance)
105+
// }
106+
}
107+
108+
function pruneCache(filter?: (name: string) => boolean) {
109+
cache.forEach((cachedInstance, key) => {
110+
const name = cachedInstance.type.name
111+
if (name && (!filter || !filter(name))) {
112+
pruneCacheEntry(key)
113+
}
114+
})
115+
}
116+
117+
function pruneCacheEntry(key: CacheKey) {
118+
const cached = cache.get(key)
119+
if (!current || cached !== current) {
120+
// TODO:
121+
// unmount(cached)
122+
} else if (current) {
123+
// current active instance should no longer be kept-alive.
124+
// we can't unmount it now but it might be later, so reset its flag now.
125+
// TODO:
126+
// resetShapeFlag(current)
127+
}
128+
cache.delete(key)
129+
keys.delete(key)
130+
}
131+
132+
// prune cache on include/exclude prop change
133+
watch(
134+
() => [props.include, props.exclude],
135+
([include, exclude]) => {
136+
include && pruneCache(name => matches(include, name))
137+
exclude && pruneCache(name => !matches(exclude, name))
138+
},
139+
// prune post-render after `current` has been updated
140+
{ flush: 'post', deep: true },
141+
)
142+
143+
// cache sub tree after render
144+
let pendingCacheKey: CacheKey | null = null
145+
const cacheSubtree = () => {
146+
// fix #1621, the pendingCacheKey could be 0
147+
if (pendingCacheKey != null) {
148+
cache.set(pendingCacheKey, instance)
149+
}
150+
}
151+
onMounted(cacheSubtree)
152+
onUpdated(cacheSubtree)
153+
154+
onBeforeUnmount(() => {
155+
// TODO:
156+
})
157+
158+
// TODO: effects
159+
return () => {
160+
pendingCacheKey = null
161+
162+
if (!slots.default) {
163+
return null
164+
}
165+
166+
const children = slots.default()
167+
const childInstance = children as ComponentInternalInstance
168+
if (isArray(children) && children.length > 1) {
169+
if (__DEV__) {
170+
warn(`KeepAlive should contain exactly one component child.`)
171+
}
172+
current = null
173+
return children
174+
} else {
175+
// TODO:
176+
}
177+
178+
const name = childInstance.type.name
179+
const { include, exclude, max } = props
180+
181+
if (
182+
(include && (!name || !matches(include, name))) ||
183+
(exclude && name && matches(exclude, name))
184+
) {
185+
return (current = childInstance)
186+
}
187+
188+
const key = childInstance.type // TODO: vnode key
189+
const cachedBlock = cache.get(key)
190+
191+
pendingCacheKey = key
192+
193+
if (cachedBlock) {
194+
// TODO: setTransitionHooks
195+
196+
keys.delete(key)
197+
keys.add(key)
198+
} else {
199+
keys.add(key)
200+
if (max && keys.size > parseInt(max as string, 10)) {
201+
pruneCacheEntry(keys.values().next().value)
202+
}
203+
}
204+
205+
return (current = childInstance)
206+
}
207+
},
208+
}
209+
210+
export const KeepAlive = KeepAliveImpl as any as {
211+
__isKeepAlive: true
212+
new (): {
213+
$props: KeepAliveProps
214+
$slots: {
215+
default(): Block
216+
}
217+
}
218+
}
219+
220+
function matches(pattern: MatchPattern, name: string): boolean {
221+
if (isArray(pattern)) {
222+
return pattern.some((p: string | RegExp) => matches(p, name))
223+
} else if (isString(pattern)) {
224+
return pattern.split(',').includes(name)
225+
} else if (isRegExp(pattern)) {
226+
return pattern.test(name)
227+
}
228+
/* istanbul ignore next */
229+
return false
230+
}

0 commit comments

Comments
 (0)