KeepAlive.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. import {
  2. Component,
  3. getCurrentInstance,
  4. FunctionalComponent,
  5. SetupContext,
  6. ComponentInternalInstance,
  7. LifecycleHooks,
  8. currentInstance
  9. } from '../component'
  10. import { VNode, cloneVNode, isVNode, VNodeProps } from '../vnode'
  11. import { warn } from '../warning'
  12. import { onBeforeUnmount, injectHook, onUnmounted } from '../apiLifecycle'
  13. import {
  14. isString,
  15. isArray,
  16. ShapeFlags,
  17. remove,
  18. invokeArrayFns
  19. } from '@vue/shared'
  20. import { watch } from '../apiWatch'
  21. import { SuspenseBoundary } from './Suspense'
  22. import {
  23. RendererInternals,
  24. queuePostRenderEffect,
  25. MoveType,
  26. RendererElement,
  27. RendererNode
  28. } from '../renderer'
  29. import { setTransitionHooks } from './BaseTransition'
  30. type MatchPattern = string | RegExp | string[] | RegExp[]
  31. export interface KeepAliveProps {
  32. include?: MatchPattern
  33. exclude?: MatchPattern
  34. max?: number | string
  35. }
  36. type CacheKey = string | number | Component
  37. type Cache = Map<CacheKey, VNode>
  38. type Keys = Set<CacheKey>
  39. export interface KeepAliveSink {
  40. renderer: RendererInternals
  41. parentSuspense: SuspenseBoundary | null
  42. activate: (
  43. vnode: VNode,
  44. container: RendererElement,
  45. anchor: RendererNode | null,
  46. isSVG: boolean,
  47. optimized: boolean
  48. ) => void
  49. deactivate: (vnode: VNode) => void
  50. }
  51. export const isKeepAlive = (vnode: VNode): boolean =>
  52. (vnode.type as any).__isKeepAlive
  53. const KeepAliveImpl = {
  54. name: `KeepAlive`,
  55. // Marker for special handling inside the renderer. We are not using a ===
  56. // check directly on KeepAlive in the renderer, because importing it directly
  57. // would prevent it from being tree-shaken.
  58. __isKeepAlive: true,
  59. props: {
  60. include: [String, RegExp, Array],
  61. exclude: [String, RegExp, Array],
  62. max: [String, Number]
  63. },
  64. setup(props: KeepAliveProps, { slots }: SetupContext) {
  65. const cache: Cache = new Map()
  66. const keys: Keys = new Set()
  67. let current: VNode | null = null
  68. const instance = getCurrentInstance()!
  69. // KeepAlive communicates with the instantiated renderer via the "sink"
  70. // where the renderer passes in platform-specific functions, and the
  71. // KeepAlive instance exposes activate/deactivate implementations.
  72. // The whole point of this is to avoid importing KeepAlive directly in the
  73. // renderer to facilitate tree-shaking.
  74. const sink = instance.sink as KeepAliveSink
  75. const {
  76. renderer: {
  77. p: patch,
  78. m: move,
  79. um: _unmount,
  80. o: { createElement }
  81. },
  82. parentSuspense
  83. } = sink
  84. const storageContainer = createElement('div')
  85. sink.activate = (vnode, container, anchor, isSVG, optimized) => {
  86. const child = vnode.component!
  87. move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
  88. // in case props have changed
  89. patch(
  90. child.vnode,
  91. vnode,
  92. container,
  93. anchor,
  94. instance,
  95. parentSuspense,
  96. isSVG,
  97. optimized
  98. )
  99. queuePostRenderEffect(() => {
  100. child.isDeactivated = false
  101. if (child.a) {
  102. invokeArrayFns(child.a)
  103. }
  104. }, parentSuspense)
  105. }
  106. sink.deactivate = (vnode: VNode) => {
  107. move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
  108. queuePostRenderEffect(() => {
  109. const component = vnode.component!
  110. if (component.da) {
  111. invokeArrayFns(component.da)
  112. }
  113. component.isDeactivated = true
  114. }, parentSuspense)
  115. }
  116. function unmount(vnode: VNode) {
  117. // reset the shapeFlag so it can be properly unmounted
  118. vnode.shapeFlag = ShapeFlags.STATEFUL_COMPONENT
  119. _unmount(vnode, instance, parentSuspense)
  120. }
  121. function pruneCache(filter?: (name: string) => boolean) {
  122. cache.forEach((vnode, key) => {
  123. const name = getName(vnode.type as Component)
  124. if (name && (!filter || !filter(name))) {
  125. pruneCacheEntry(key)
  126. }
  127. })
  128. }
  129. function pruneCacheEntry(key: CacheKey) {
  130. const cached = cache.get(key) as VNode
  131. if (!current || cached.type !== current.type) {
  132. unmount(cached)
  133. } else if (current) {
  134. // current active instance should no longer be kept-alive.
  135. // we can't unmount it now but it might be later, so reset its flag now.
  136. current.shapeFlag = ShapeFlags.STATEFUL_COMPONENT
  137. }
  138. cache.delete(key)
  139. keys.delete(key)
  140. }
  141. watch(
  142. () => [props.include, props.exclude],
  143. ([include, exclude]) => {
  144. include && pruneCache(name => matches(include, name))
  145. exclude && pruneCache(name => matches(exclude, name))
  146. }
  147. )
  148. onBeforeUnmount(() => {
  149. cache.forEach(unmount)
  150. })
  151. return () => {
  152. if (!slots.default) {
  153. return null
  154. }
  155. const children = slots.default()
  156. let vnode = children[0]
  157. if (children.length > 1) {
  158. if (__DEV__) {
  159. warn(`KeepAlive should contain exactly one component child.`)
  160. }
  161. current = null
  162. return children
  163. } else if (
  164. !isVNode(vnode) ||
  165. !(vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT)
  166. ) {
  167. current = null
  168. return vnode
  169. }
  170. const comp = vnode.type as Component
  171. const name = getName(comp)
  172. const { include, exclude, max } = props
  173. if (
  174. (include && (!name || !matches(include, name))) ||
  175. (exclude && name && matches(exclude, name))
  176. ) {
  177. return vnode
  178. }
  179. const key = vnode.key == null ? comp : vnode.key
  180. const cachedVNode = cache.get(key)
  181. // clone vnode if it's reused because we are going to mutate it
  182. if (vnode.el) {
  183. vnode = cloneVNode(vnode)
  184. }
  185. cache.set(key, vnode)
  186. if (cachedVNode) {
  187. // copy over mounted state
  188. vnode.el = cachedVNode.el
  189. vnode.component = cachedVNode.component
  190. if (vnode.transition) {
  191. // recursively update transition hooks on subTree
  192. setTransitionHooks(vnode, vnode.transition!)
  193. }
  194. // avoid vnode being mounted as fresh
  195. vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
  196. // make this key the freshest
  197. keys.delete(key)
  198. keys.add(key)
  199. } else {
  200. keys.add(key)
  201. // prune oldest entry
  202. if (max && keys.size > parseInt(max as string, 10)) {
  203. pruneCacheEntry(Array.from(keys)[0])
  204. }
  205. }
  206. // avoid vnode being unmounted
  207. vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
  208. current = vnode
  209. return vnode
  210. }
  211. }
  212. }
  213. // export the public type for h/tsx inference
  214. // also to avoid inline import() in generated d.ts files
  215. export const KeepAlive = (KeepAliveImpl as any) as {
  216. new (): {
  217. $props: VNodeProps & KeepAliveProps
  218. }
  219. }
  220. function getName(comp: Component): string | void {
  221. return (comp as FunctionalComponent).displayName || comp.name
  222. }
  223. function matches(pattern: MatchPattern, name: string): boolean {
  224. if (isArray(pattern)) {
  225. return (pattern as any).some((p: string | RegExp) => matches(p, name))
  226. } else if (isString(pattern)) {
  227. return pattern.split(',').indexOf(name) > -1
  228. } else if (pattern.test) {
  229. return pattern.test(name)
  230. }
  231. /* istanbul ignore next */
  232. return false
  233. }
  234. export function onActivated(
  235. hook: Function,
  236. target?: ComponentInternalInstance | null
  237. ) {
  238. registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target)
  239. }
  240. export function onDeactivated(
  241. hook: Function,
  242. target?: ComponentInternalInstance | null
  243. ) {
  244. registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target)
  245. }
  246. function registerKeepAliveHook(
  247. hook: Function & { __wdc?: Function },
  248. type: LifecycleHooks,
  249. target: ComponentInternalInstance | null = currentInstance
  250. ) {
  251. // cache the deactivate branch check wrapper for injected hooks so the same
  252. // hook can be properly deduped by the scheduler. "__wdc" stands for "with
  253. // deactivation check".
  254. const wrappedHook =
  255. hook.__wdc ||
  256. (hook.__wdc = () => {
  257. // only fire the hook if the target instance is NOT in a deactivated branch.
  258. let current: ComponentInternalInstance | null = target
  259. while (current) {
  260. if (current.isDeactivated) {
  261. return
  262. }
  263. current = current.parent
  264. }
  265. hook()
  266. })
  267. injectHook(type, wrappedHook, target)
  268. // In addition to registering it on the target instance, we walk up the parent
  269. // chain and register it on all ancestor instances that are keep-alive roots.
  270. // This avoids the need to walk the entire component tree when invoking these
  271. // hooks, and more importantly, avoids the need to track child components in
  272. // arrays.
  273. if (target) {
  274. let current = target.parent
  275. while (current && current.parent) {
  276. if (isKeepAlive(current.parent.vnode)) {
  277. injectToKeepAliveRoot(wrappedHook, type, target, current)
  278. }
  279. current = current.parent
  280. }
  281. }
  282. }
  283. function injectToKeepAliveRoot(
  284. hook: Function,
  285. type: LifecycleHooks,
  286. target: ComponentInternalInstance,
  287. keepAliveRoot: ComponentInternalInstance
  288. ) {
  289. injectHook(type, hook, keepAliveRoot, true /* prepend */)
  290. onUnmounted(() => {
  291. remove(keepAliveRoot[type]!, hook)
  292. }, target)
  293. }