一.在vue-router中使用,保活一个路由组件。
1. 一般写法
vue3中,对于这个问题,写法有点不一样。
<router-view>、<keep-alive> 和 <transition>
transition 和 keep-alive 现在必须通过 v-slot API 在 RouterView 内部使用,下面是一个案例:
<router-view v-slot="{ Component,route }"> <transition> <keep-alive> <component :is="Component" v-if="route.meta.keepalive==true" :key="route.path" /> </keep-alive> <component :is="Component" v-if="route.meta.keepalive==false" :key="route.path" /> </transition> </router-view>
原因: 这是一个必要的变化。详见 related RFC.
所以说这里还有其他的信息,transition过度效果,现在也要用这种方式来写了。
- 回顾一下插槽:
其中v-slot="{ Component }"这种写法,是解构插槽 Prop,用来解构作用域插槽的参数,作用域插槽是用来向组件提供插槽属性的:绑定在 <slot> 元素上的 attribute 被称为插槽 prop。现在,在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:
// 一个todo-list组件,有一个默认的插槽 <ul> <li v-for="( item, index ) in items"> <slot :item="item" :index="index" :another-attribute="anotherAttribute"></slot> </li> </ul> // 作用域插槽 <todo-list> <template v-slot:default="slotProps"> <i class="fas fa-check"></i> <span class="green">{{ slotProps.item }}</span> </template> </todo-list>
2.推荐写法
Props: include - string | RegExp | Array。只有名称匹配的组件会被缓存。 exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存。 max - number | string。最多可以缓存多少组件实例。
keepAlive本身具有的include去匹配。
比如我有一个routes:
const routes = [{ path:"/", component:layout, redirect:"/AccessibleMap", children:[ { path:"page404", name:"page404", meta:{title:"page404",ismenu:false,keepalive:false}, component:()=> import("../pages/page404.vue") }, { path:"AccessibleMap", name:"AccessibleMap", meta:{title:"第一个例子",ismenu:true,keepalive:true}, component:()=> import("../pages/AccessibleMap.vue") }, { path:"tianditu", name:"tianditu", meta:{title:"加载天地图",ismenu:true,keepalive:true}, component:()=> import("../pages/tianditu.vue") }, { path:"baohuo", name:"baohuo", meta:{title:"保活组件",ismenu:true,keepalive:true}, component:()=> import("../pages/baohuo.vue") } ] }];
在组件里面去引用,只需要过滤出一个需要保活的name数据就可以,比如说“baohuo”这个组件,组件要定义name属性(并不是指上面路由表routes里面那个name,而是baohuo.vue的name)
匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components
选项的键值)。匿名组件不能被匹配。
<template> <div class="catalog"> <router-link class="mylink" active-class="my-active" v-for="item in link" :to="`/${item.path}`"> <span v-if="item.meta.ismenu == true">{{ item.meta.title }}</span> </router-link> </div> <router-view v-slot="{ Component, route }"> <keep-alive :include="keepaliveRoutes"> <component :is="Component" :key="route.path" /> </keep-alive> </router-view> </template> <script lang="ts"> import { defineComponent } from "vue"; import { routes } from "../router" interface idata { link: { path: string; meta: { title: string, ismenu: boolean, keepalive: boolean } }[]; keepaliveRoutes: Array<string>, } export default defineComponent({ data(): idata { return { link: [], keepaliveRoutes:[], } }, components: { }, created() { this.link = routes[0].children.map(e => { return { path: e.path, meta: e.meta } }) routes[0].children.forEach(e=>{ if(e.meta.keepalive == true){ this.keepaliveRoutes.push(e.path) } }) }, }); </script>
二.普通组件的保活
跟上面说的类似。
三、源码分析
日常开发中,如果需要在组件切换时,保存组件的状态,防止它多次销毁,多次渲染,我们通常采用 <keep-alive>
组件处理,因为它能够缓存不活动的组件,而不是销毁它们。同时, <keep-alive>
组件不会渲染自己的 DOM 元素,也不会出现在组件父链中,属于一个抽象组件。当组件在 <keep-alive>
内被切换时,它的 activated
和 deactivated
这两个钩子函数将会被对应执行。
基础用法
以下是 <keep-alive>
组件的示例用法,
<keep-alive :include="['a', 'b']" :max="10"> <component :is="view"></component> </keep-alive>
复制代码
属性 Props
- include 字符串或表达式。只有名称匹配的组件会被缓存。
- exclude 字符串或正则表达式。任务名称匹配的组件都不会被缓存。
- max 数字。最多可以缓存多少组件实例。
注意的是, <keep-alive>
组件是用在直属的子组件被开关的情况,若存在多条件性的子元素,则要求同时只能有一个元素被渲染。
组件源码实现
上面我们了解了 <keep-alive>
组件的定义、属性以及用法,下面就看下源码是如何对应实现的。
抽象组件
我们去掉多余的代码,看看 KeepAlive 组件是如何定义的。
const KeepAliveImpl = { __isKeepAlive: true, inheritRef: true, props: { include: [String, RegExp, Array], exclude: [String, RegExp, Array], max: [String, Number] }, setup(props: KeepAliveProps, { slots }: SetupContext){ // 省略其他代码... return()=>{ if (!slots.default) { return null } // 拿到组件的子节点 const children = slots.default() // 取第一个子节点 let vnode = children[0] // 存在多个子节点的时候,keepAlive组件不生效了,直接返回 if (children.length > 1) { current = null return children } else if ( !isVNode(vnode) || !(vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) ) { current = null return vnode } // 省略其他代码... // 返回第一个子节点 return vnode } } }
从源码可以看出 KeepAlive 组件是通过 Composition API 实现的,setup 返回的是组件的渲染函数。在渲染函数内,取组件的子节点,当存在多个子节点,则直接返回所有节点,也就 KeepAlive 组件不生效了。当仅存在一个子节点,则渲染第一个子节点的内容,也就验证了 KeepAlive 是抽象组件,不渲染本身的 DOM 元素。
缓存机制
了解 KeepAlive 组件缓存机制前,我们先了解下 LRU 算法概念,它正是通过该算法来处理缓存机制。
LRU 算法
我们常用缓存来提升数据查询的数据,由于缓存容量有限,当缓存容量到达上限,就需要删除部分数据挪出空间,让新数据添加进来。因此需要制定一些策略对加入缓存的数据进行管理。常见的策略有:
- LUR 最近最久未使用
- FIFO 先进先出
- NRU Clock 置换算法
- LFU 最少使用置换算法
- PBA 页面缓冲算法
KeepAlive 缓存机制使用的是 LRU 算法(Least Recently Used),当数据在最近一段时间被访问,那么它在以后也会被经常访问。这就意味着,如果经常访问的数据,我们需要能够快速命中,而不常访问的数据,我们在容量超出限制,要将其淘汰。
我们这里只讲概念,如果想深入理解 LRU 算法,可自行查找。
缓存实现
简化下代码,抽离出核心代码,看看缓存机制
const KeepAliveImpl = { setup(props){ // 缓存KeepAlive子节点的数据结构{key:vNode} const cache: Cache = new Map() // 保存KeepAlive子节点唯一标识的数据结构 const keys: Keys = new Set() let current: VNode | null = null let pendingCacheKey: CacheKey | null = null // 在beforeMount/Update 缓存子树 const cacheSubtree = () => { if (pendingCacheKey != null) { cache.set(pendingCacheKey, instance.subTree) } } onBeforeMount(cacheSubtree) onBeforeUpdate(cacheSubtree) return ()=>{ pendingCacheKey = null const children = slots.default() let vnode = children[0] const comp = vnode.type as Component const name = getName(comp) // 解构出属性值 const { include, exclude, max } = props // key值是KeepAlive子节点创建时添加的,作为缓存节点的唯一标识 const key = vnode.key == null ? comp : vnode.key // 通过key值获取缓存节点 const cachedVNode = cache.get(key) if (cachedVNode) { // 缓存存在,则使用缓存装载数据 vnode.el = cachedVNode.el vnode.component = cachedVNode.component if (vnode.transition) { // 递归更新子树上的 transition hooks setTransitionHooks(vnode, vnode.transition!) } // 阻止vNode节点作为新节点被挂载 vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE // 让key始终新鲜 keys.delete(key) keys.add(key) } else { keys.add(key) // 属性配置max值,删除最久不用的key,这很符合LRU的思想 if (max && keys.size > parseInt(max as string, 10)) { pruneCacheEntry(keys.values().next().value) } } // 避免vNode被卸载 vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE current = vnode return vnode; } } }
从源码中可以看出 KeepAlive 声明了了个 cache 变量来缓存节点数据,它是 Map 结构。并采用 LRU 缓存算法来处理子节点存储机制,具体说明如下:
- 声明有序集合 keys 作为缓存容器,容器内缓存组件的唯一标识 key
- keys 缓存容器中的数据,越靠前的 key 值越少被访问越旧,往后的值越新鲜
- 渲染函数执行时,若命中缓存时,则从 keys 中删除当前命中的 key,并往 keys 末尾追加 key 值,保存新鲜
- 未命中缓存时,则 keys 追加缓存数据 key 值,若此时缓存数据长度大于 max 最大值,则删除最旧的数据,这里的值是 keys 中第一个值,很符合 LRU 思想。
- 当触发 beforeMount/update 生命周期,缓存当前激活的子树的数据
挂载区别
通常组件挂载、卸载都会触发各自生命周期,那 KeepAlive 子树有无缓存在挂载阶段是否存在区别呢?以下抽离下 patch 阶段中 ShapeFlags.COMPONENT
类型相关核心代码看看。
const processComponent = (n1: VNode | null,n2: VNode,container: RendererElement,anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null,parentSuspense: SuspenseBoundary | null, isSVG: boolean,optimized: boolean ) => { if (n1 == null) { // 存在COMPONENT_KEPT_ALIVE ,激活n2 if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { ;(parentComponent!.ctx as KeepAliveContext).activate(n2,container,anchor,isSVG,optimized) } else { // 否则,挂载组件 mountComponent(n2,container,anchor,parentComponent,parentSuspense,isSVG,optimized) } } else { // 更新组件 updateComponent(n1, n2, optimized) } }
KeepAlive 组件在渲染函数执行时,若存在缓存,会给 vNode 赋予 vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
状态,因此再次渲染该子树时,会执行parentComponent!.ctx.activate
函数激活子树的状态。那这里的 activate
函数是什么呢?看下代码
const instance = getCurrentInstance() const sharedContext = instance.ctx as KeepAliveContext sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => { const instance = vnode.component! // 挂载节点 move(vnode, container, anchor, MoveType.ENTER, parentSuspense) // 更新组件,可能存在props发生变化 patch(instance.vnode,vnode,container,anchor,instance,parentSuspense,isSVG,optimized) queuePostRenderEffect(() => { // 组件渲染完成后,执行子节点组件定义的actived钩子函数 instance.isDeactivated = false if (instance.a) {invokeArrayFns(instance.a)} const vnodeHook = vnode.props && vnode.props.onVnodeMounted if (vnodeHook) { invokeVNodeHook(vnodeHook, instance.parent, vnode) } }, parentSuspense) }
再次激活子树时,因为上次渲染已经缓存了 vNode
,能够从 vNode 直接获取缓存的 DOM 了,也就无需再次转次 vNode。因此可以直接执行 move
挂载子树,然后再执行 patch 更新组件,最后再通过queuePostRenderEffec
t
,在组件渲染完成后,执行子节点组件定义的 activate
钩子函数。
再看下激活/失效的实现思路,通过将渲染器传入 KeepAlive 实例的 ctx 属性内部,实现 KeepAlive 与渲染器实例的通信,并且通过 KeepAlive 暴露 acttivate/deactivate 两个实现。这样做的目的是,避免在渲染器直接导入 KeepAlive 产生 tree-shaking
。
属性实现
KeepAlive 支持 3 个属性 include,exclude,max。其中 max 在上面已经讲过了,这里看下另外 2 个属性的实现。
setup(){ watch( () => [props.include, props.exclude], ([include, exclude]) => { include && pruneCache(name => matches(include, name)) exclude && pruneCache(name => matches(exclude, name)) } ) return ()=>{ if ( (include && (!name || !matches(include, name))) || (exclude && name && matches(exclude, name)) ) { return (current = vnode) } } }
这里很好理解,当子组件名称不匹配 include 的配置值,或者子组件名称匹配了 exclude 的值,都不该被缓存,而是直接返回。而 watch 函数是监听 include、exclude 值变化时做出对应反应,即去删除对应的缓存数据。
卸载过程
1.卸载分为子组件切换时产生的子组件卸载流程,以及 KeepAlive 组件卸载导致的卸载流程。
子组件卸载流程组件卸载过程,会执行 unmount 方法,然后执行 parentComponent.ctx.deactivate(vnode)函数,在函数里通过 move 函数移除节点,然后通过 queuePostRenderEffect 的方式执行定义的deactivated 钩子函数。此过程跟挂载过程类似,不过多描述。
2.KeepAlive 组件卸载当 KeepAlive 组件卸载时,会触发 onBeforeUnmount 函数,现在看看该函数的实现:
onBeforeUnmount(() => { cache.forEach(cached => { const { subTree, suspense } = instance if (cached.type === subTree.type) { resetShapeFlag(subTree) const da = subTree.component!.da da && queuePostRenderEffect(da, suspense) return } unmount(cached) }) })
当缓存的 vnode 为当前 KeepAlive 组件渲染的 vnode 时,重置 vnode 的 ShapeFlag,让它不被当做是 KeepAlive 的 vNode,然后通过 queuePostRenderEffect 执行子组件的 deactivated 函数,这样就完成了卸载逻辑。否则,则执行 unmount 方法执行 vnode 的整套卸载路程。
附:LRU 算法
class LRUCache{ constructor(capacity){ this.capacity = capacity || 2 this.cache = new Map() } // 存值,超出最大则默认删除第一个:最近最少被用元素 put(key,val){ if(this.cache.has(key)){ this.cache.delete(key) } if(this.cache.size>=this.capacity){ this.cache.delete(this.cache.keys().next().value) } this.cache.set(key,val) } // 取值,同时刷新缓存新鲜度 get(key){ if(this.cache.has(key)){ const temp = this.cache.get(key) this.cache.delete(key) this.cache.set(key,temp) return temp } return -1 } }