KeepAlive.ts 11 KB

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