侧边栏壁纸
  • 累计撰写 23 篇文章
  • 累计收到 72 条评论
Vue

keep-alive

Zuiet
2022-09-17 / 0 评论 / 416 阅读 / 正在检测是否收录...

keep-alive在vue-router中使用,保活一个路由组件。

一般写法

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,用来解构作用域插槽的参数,作用域插槽是用来向组件提供插槽属性的:绑定在 元素上的 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>

推荐写法

keep-alive

Props:
include - string | RegExp | Array。只有名称匹配的组件会被缓存。
exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存。
max - number | string。最多可以缓存多少组件实例。

keepAlive本身具有的include去匹配。
比如我有一个routes:

 onst 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>内被切换时,它的 activateddeactivated 这两个钩子函数将会被对应执行。
基础用法
以下是 <keep-alive> 组件的示例用法

<keep-alive :include="['a', 'b']" :max="10">
   <component :is="view"></component>
</keep-alive>

属性 Props

  1. include 字符串或表达式。只有名称匹配的组件会被缓存。
  2. exclude 字符串或正则表达式。任务名称匹配的组件都不会被缓存。
  3. 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 算法
我们常用缓存来提升数据查询的数据,由于缓存容量有限,当缓存容量到达上限,就需要删除部分数据挪出空间,让新数据添加进来。因此需要制定一些策略对加入缓存的数据进行管理。常见的策略有:

  1. LUR 最近最久未使用
  2. FIFO 先进先出
  3. NRU Clock 置换算法
  4. LFU 最少使用置换算法
  5. 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 缓存算法来处理子节点存储机制,具体说明如下:

  1. 声明有序集合 keys 作为缓存容器,容器内缓存组件的唯一标识 key
  2. keys 缓存容器中的数据,越靠前的 key 值越少被访问越旧,往后的值越新鲜
  3. 渲染函数执行时,若命中缓存时,则从 keys 中删除当前命中的 key,并往 keys 末尾追加 key 值,保存新鲜
  4. 未命中缓存时,则 keys 追加缓存数据 key 值,若此时缓存数据长度大于 max 最大值,则删除最旧的数据,这里的值是
    keys中第一个值,很符合 LRU 思想。
  5. 当触发 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 更新组件,最后再通过queuePostRenderEffect,在组件渲染完成后,执行子节点组件定义的activate钩子函数。

再看下激活/失效的实现思路,通过将渲染器传入 KeepAlive 实例的 ctx 属性内部,实现 KeepAlive 与渲染器实例的通信,并且通过 KeepAlive 暴露 activate/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 值变化时做出对应反应,即去删除对应的缓存数据。
卸载过程
卸载分为子组件切换时产生的子组件卸载流程,以及 KeepAlive 组件卸载导致的卸载流程

  1. 子组件卸载流程组件卸载过程,会执行 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
  }
}
0

评论 (0)

取消