• Vue3 源码阅读(12):组件化 —— KeepAlive


    这篇博客讲讲我们经常使用的 KeepAlive 组件,该组件对应的官方文档点击这里

    1,实现思路

    KeepAlive 组件的作用是:缓存子组件的实例以及上次渲染的真实 DOM,当 KeepAlive 的子组件是之前已经缓存的子组件时,会将缓存的真实 DOM 和组件实例拿出来进行复原,这可以防止子组件从 0 开始实例化和渲染。

    KeepAlive 组件的实现也是借助 setup 函数返回组件 render 函数实现的。在 setup 函数中,可以通过 SetupContext 中的 slots 获取子组件的 VNode,这是能够进行组件缓存的基础。

    KeepAlive 第一次渲染某个子组件时的操作

    当 KeepAlive 组件第一次渲染某一个子组件时,这个子组件还没有被缓存,KeepAlive 组件的 render 函数会返回这个子组件的 VNode,并将这个子组件 VNode 缓存到一个 Map 实例中。接下来就到了渲染器的工作环节了,渲染器会根据子组件的 VNode 进行子组件实例的创建以及真实 DOM 的渲染,渲染器会将组件的实例以及渲染的真实 DOM 赋值到 VNode 的 component 和 el 属性上。

    KeepAlive 渲染的子组件 deactivate 时的操作

    KeepAlive 会将 deactivate 子组件的真实 DOM 移动到一个隐藏 DOM 中,这可以达到隐藏 DOM 的目的。

    KeepAlive deactivate 的子组件重新 activate 时做的操作

    如果 KeepAlive 的子组件是之前已经缓存过的组件的话,KeepAlive 会根据 vnode 的 key 从缓存中获取对应的 cachedVNode,将 cachedVNode 的 el 和 component 属性设置到 vnode 上,并且会将一个特定的标识设置到 vnode 的 shapeFlag 属性上。接下来到了渲染器处理的阶段,当渲染器发现处理的 VNode 是 KeepAlive 缓存过的时,会使用 KeepAlive 中定义的工具方法进行处理,这个工具方法会将这个组件之前渲染的真实 DOM 从隐藏 DOM 中移动到页面中。

    2,源码解读

    KeepAlive 组件的源码如下所示,请根据上一小节说的思路以及注释进行理解。

    1. const KeepAliveImpl: ComponentOptions = {
    2. name: `KeepAlive`,
    3. // Marker for special handling inside the renderer. We are not using a ===
    4. // check directly on KeepAlive in the renderer, because importing it directly
    5. // would prevent it from being tree-shaken.
    6. __isKeepAlive: true,
    7. props: {
    8. include: [String, RegExp, Array],
    9. exclude: [String, RegExp, Array],
    10. max: [String, Number]
    11. },
    12. setup(props: KeepAliveProps, { slots }: SetupContext) {
    13. // 获取当前的 KeepAlive 组件实例
    14. const instance = getCurrentInstance()!
    15. // 获取组件实例的 ctx 属性,这个属性对象中存储有渲染器
    16. const sharedContext = instance.ctx as KeepAliveContext
    17. // 用于缓存组件的一个 Map 实例,键是组件 VNode 的 key,值是组件的 VNode
    18. const cache: Cache = new Map()
    19. // 用于存储缓存组件 key 的 Set 实例
    20. const keys: Keys = new Set()
    21. // 用于存储当前 KeepAlive 组件的子组件的 VNode
    22. let current: VNode | null = null
    23. const parentSuspense = instance.suspense
    24. // 获取 sharedContext 中保存的渲染器方法
    25. const {
    26. renderer: {
    27. p: patch,
    28. m: move,
    29. um: _unmount,
    30. o: { createElement }
    31. }
    32. } = sharedContext
    33. // 用于存储缓存组件 DOM 的容器
    34. const storageContainer = createElement('div')
    35. // 封装两个工具函数到 instance.ctx 中,这两个工具函数会在渲染器中的 processComponent 和 unmount 函数中使用
    36. // 当渲染器发现 VNode 是经过 KeepAlive 处理缓存过的话,会使用这两个自定义函数进行处理,不会使用渲染器中的默认操作进行处理
    37. sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
    38. const instance = vnode.component!
    39. // 将之前已经渲染的 DOM 从 storageContainer 中移动到 container 中
    40. move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
    41. // in case props have changed
    42. patch(
    43. instance.vnode,
    44. vnode,
    45. container,
    46. anchor,
    47. instance,
    48. parentSuspense,
    49. isSVG,
    50. vnode.slotScopeIds,
    51. optimized
    52. )
    53. queuePostRenderEffect(() => {
    54. instance.isDeactivated = false
    55. if (instance.a) {
    56. invokeArrayFns(instance.a)
    57. }
    58. const vnodeHook = vnode.props && vnode.props.onVnodeMounted
    59. if (vnodeHook) {
    60. invokeVNodeHook(vnodeHook, instance.parent, vnode)
    61. }
    62. }, parentSuspense)
    63. }
    64. sharedContext.deactivate = (vnode: VNode) => {
    65. const instance = vnode.component!
    66. // 将失活组件的真实 DOM 隐藏到 storageContainer 中
    67. move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
    68. queuePostRenderEffect(() => {
    69. if (instance.da) {
    70. invokeArrayFns(instance.da)
    71. }
    72. const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
    73. if (vnodeHook) {
    74. invokeVNodeHook(vnodeHook, instance.parent, vnode)
    75. }
    76. instance.isDeactivated = true
    77. }, parentSuspense)
    78. }
    79. function unmount(vnode: VNode) {
    80. // reset the shapeFlag so it can be properly unmounted
    81. resetShapeFlag(vnode)
    82. _unmount(vnode, instance, parentSuspense, true)
    83. }
    84. function pruneCache(filter?: (name: string) => boolean) {
    85. cache.forEach((vnode, key) => {
    86. const name = getComponentName(vnode.type as ConcreteComponent)
    87. if (name && (!filter || !filter(name))) {
    88. pruneCacheEntry(key)
    89. }
    90. })
    91. }
    92. function pruneCacheEntry(key: CacheKey) {
    93. const cached = cache.get(key) as VNode
    94. if (!current || cached.type !== current.type) {
    95. unmount(cached)
    96. } else if (current) {
    97. // current active instance should no longer be kept-alive.
    98. // we can't unmount it now but it might be later, so reset its flag now.
    99. resetShapeFlag(current)
    100. }
    101. cache.delete(key)
    102. keys.delete(key)
    103. }
    104. // 监控 include 和 exclude 响应式属性,当这两个属性发生变化的时候,对缓存的组件进行修剪。
    105. // 只有组件在 include 中,并不在 exclude 中时,组件才能够被 KeepAlive 缓存。
    106. watch(
    107. () => [props.include, props.exclude],
    108. ([include, exclude]) => {
    109. // 根据最新的 include 和 exclude 进行组件缓存的修剪
    110. include && pruneCache(name => matches(include, name))
    111. exclude && pruneCache(name => !matches(exclude, name))
    112. },
    113. // prune post-render after `current` has been updated
    114. { flush: 'post', deep: true }
    115. )
    116. // cache sub tree after render
    117. let pendingCacheKey: CacheKey | null = null
    118. // 缓存 VNode 的工具函数
    119. const cacheSubtree = () => {
    120. // fix #1621, the pendingCacheKey could be 0
    121. if (pendingCacheKey != null) {
    122. // 缓存的 VNode 是当前 KeepAlive 组件实例的 subTree 属性
    123. // KeepAlive 组件的 render 函数返回的 VNode 是子组件的 VNode,
    124. // 但是在渲染器的视角来看,是谁的 render 函数返回的 VNode,那么这个 VNode 就是属于那个组件实例,所以会将上次渲染的 VNode
    125. // 设置到当前 KeepAlive 组件实例的 subTree 属性上
    126. cache.set(pendingCacheKey, getInnerChild(instance.subTree))
    127. }
    128. }
    129. // 在组件挂载和组件升级的时候进行组件的缓存操作
    130. onMounted(cacheSubtree)
    131. onUpdated(cacheSubtree)
    132. onBeforeUnmount(() => {
    133. cache.forEach(cached => {
    134. const { subTree, suspense } = instance
    135. const vnode = getInnerChild(subTree)
    136. if (cached.type === vnode.type) {
    137. // current instance will be unmounted as part of keep-alive's unmount
    138. resetShapeFlag(vnode)
    139. // but invoke its deactivated hook here
    140. const da = vnode.component!.da
    141. da && queuePostRenderEffect(da, suspense)
    142. return
    143. }
    144. unmount(cached)
    145. })
    146. })
    147. // 返回 KeepAlive 的 render 函数,render 函数的作用是:返回 VNode,VNode 作为参数用于渲染器的渲染
    148. return () => {
    149. pendingCacheKey = null
    150. // 如果当前的 KeepAlive 组件没有子组件的话,直接 return 即可,不用做任何操作
    151. if (!slots.default) {
    152. return null
    153. }
    154. // 通过 slots.default() 获取当前 KeepAlive 的默认子组件信息
    155. const children = slots.default()
    156. // 获取第一个子组件
    157. const rawVNode = children[0]
    158. if (children.length > 1) {
    159. // 如果有多个子组件的话,则打印出警告,KeepAlive 只允许有一个子组件,如果有多个的话,则不进行缓存处理
    160. if (__DEV__) {
    161. warn(`KeepAlive should contain exactly one component child.`)
    162. }
    163. current = null
    164. // 直接返回子组件的 VNode
    165. return children
    166. } else if (
    167. !isVNode(rawVNode) ||
    168. (!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
    169. !(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
    170. ) {
    171. current = null
    172. return rawVNode
    173. }
    174. // 接下来进行缓存的真正处理
    175. let vnode = getInnerChild(rawVNode)
    176. const comp = vnode.type as ConcreteComponent
    177. // 获取当前子组件的 name 名称
    178. const name = getComponentName(
    179. isAsyncWrapper(vnode)
    180. ? (vnode.type as ComponentOptions).__asyncResolved || {}
    181. : comp
    182. )
    183. const { include, exclude, max } = props
    184. // 根据 name、include、exclude 判断当前的子组件是否可以进行缓存
    185. if (
    186. (include && (!name || !matches(include, name))) ||
    187. (exclude && name && matches(exclude, name))
    188. ) {
    189. current = vnode
    190. // 当前的子组件不满足缓存要求,直接返回 rawVNode
    191. return rawVNode
    192. }
    193. // 获取当前缓存的 key
    194. const key = vnode.key == null ? comp : vnode.key
    195. // 从 cache 中获取缓存的 VNode
    196. const cachedVNode = cache.get(key)
    197. // 将 key 设置到 pendingCacheKey 变量上
    198. pendingCacheKey = key
    199. // 如果 cachedVNode 存在的话,说明这个组件之前已经被缓存了,此时直接将 cachedVNode 的 el 和 component 赋值到 vnode 上即可
    200. if (cachedVNode) {
    201. // copy over mounted state
    202. vnode.el = cachedVNode.el
    203. vnode.component = cachedVNode.component
    204. // 将 vnode 的 shapeFlag 属性设置为 COMPONENT_KEPT_ALIVE,
    205. // 在渲染器中,如果发现 vnode 的 shapeFlag 属性是 COMPONENT_KEPT_ALIVE 的话,
    206. // 会使用上面定义的 sharedContext.deactivate 函数进行处理
    207. vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
    208. // make this key the freshest
    209. // 将 key 从 keys 中删除并重新添加 key,key 放在 keys 的最后意味着对应的组件是最新的
    210. // 当缓存的组件数量超过 max 时,会将缓存的最旧组件移除
    211. keys.delete(key)
    212. keys.add(key)
    213. } else {
    214. // 如果 cachedVNode 不存在的话,说明当前的子组件是第一个渲染在 KeepAlive 下面,此时需要进行缓存处理
    215. // 首先将缓存的 key 保存到 keys 中
    216. keys.add(key)
    217. // prune oldest entry
    218. // 如果当前 keys 的个数超过了 max 的话,需要将第一个 key 对应组件缓存移除掉
    219. if (max && keys.size > parseInt(max as string, 10)) {
    220. // 使用 pruneCacheEntry 函数将指定 key 对应的组件缓存移除掉
    221. pruneCacheEntry(keys.values().next().value)
    222. }
    223. }
    224. // 将 vnode 的 shapeFlag 标志设置为 COMPONENT_SHOULD_KEEP_ALIVE,这可以避免 vnode 被卸载
    225. vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
    226. // 将 vnode 设置到 current 变量上
    227. current = vnode
    228. // 最终返回用于渲染的 vnode
    229. return vnode
    230. }
    231. }
    232. }

    在下面的渲染器源码中,如果 Vue 发现当前操作的 VNode 是经过 KeepAlive 处理过的话,会使用 KeepAlive 中定义的工具函数进行处理。

    1. const processComponent = (
    2. n1: VNode | null,
    3. n2: VNode,
    4. container: RendererElement,
    5. anchor: RendererNode | null,
    6. parentComponent: ComponentInternalInstance | null,
    7. parentSuspense: SuspenseBoundary | null,
    8. isSVG: boolean,
    9. slotScopeIds: string[] | null,
    10. optimized: boolean
    11. ) => {
    12. n2.slotScopeIds = slotScopeIds
    13. if (n1 == null) {
    14. if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
    15. ;(parentComponent!.ctx as KeepAliveContext).activate(
    16. n2,
    17. container,
    18. anchor,
    19. isSVG,
    20. optimized
    21. )
    22. } else {
    23. mountComponent(
    24. n2,
    25. container,
    26. anchor,
    27. parentComponent,
    28. parentSuspense,
    29. isSVG,
    30. optimized
    31. )
    32. }
    33. } else {
    34. updateComponent(n1, n2, optimized)
    35. }
    36. }
    37. const unmount: UnmountFn = (
    38. vnode,
    39. parentComponent,
    40. parentSuspense,
    41. doRemove = false,
    42. optimized = false
    43. ) => {
    44. const {
    45. type,
    46. props,
    47. ref,
    48. children,
    49. dynamicChildren,
    50. shapeFlag,
    51. patchFlag,
    52. dirs
    53. } = vnode
    54. // unset ref
    55. if (ref != null) {
    56. setRef(ref, null, parentSuspense, vnode, true)
    57. }
    58. if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
    59. ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
    60. return
    61. }
    62. ......
    63. }

  • 相关阅读:
    Spring-Cloud-Alibaba-SEATA源码解析(三)(客户端)
    身体是革命的本钱,希望大家编程之余努力搞好身体
    Redis -- 基本知识说明
    React设计思路
    Vue.js新手指南:从零开始建立你的第一个应用
    HyperLynx(三十)高速串行总线仿真(二)
    先来聊聊MySQL的binlog文件解析
    spark查看日志
    国际通用回收标准-GRS、RCS的答疑
    本地部署AutoGPT
  • 原文地址:https://blog.csdn.net/f18855666661/article/details/126560309