vue中组件保活<keep-alive>的使用

简介: vue中组件保活<keep-alive>的使用

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

1. 一般写法

vue3中,对于这个问题,写法有点不一样。

<router-view>、<keep-alive> 和 <transition>

transitionkeep-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.推荐写法

keep-alive

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> 内被切换时,它的 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 暴露 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
    }
}

相关文章
|
25天前
|
JavaScript API 开发者
Vue是如何进行组件化的
Vue是如何进行组件化的
|
22天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
126 64
|
1天前
|
JavaScript 关系型数据库 MySQL
基于VUE的校园二手交易平台系统设计与实现毕业设计论文模板
基于Vue的校园二手交易平台是一款专为校园用户设计的在线交易系统,提供简洁高效、安全可靠的二手商品买卖环境。平台利用Vue框架的响应式数据绑定和组件化特性,实现用户友好的界面,方便商品浏览、发布与管理。该系统采用Node.js、MySQL及B/S架构,确保稳定性和多功能模块设计,涵盖管理员和用户功能模块,促进物品循环使用,降低开销,提升环保意识,助力绿色校园文化建设。
|
22天前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
28 8
|
22天前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
25天前
|
JavaScript 前端开发 开发者
Vue是如何劫持响应式对象的
Vue是如何劫持响应式对象的
23 1
|
25天前
|
JavaScript 前端开发 开发者
Vue是如何进行组件化的
Vue是如何进行组件化的
|
25天前
|
存储 JavaScript 前端开发
介绍一下Vue的核心功能
介绍一下Vue的核心功能
|
JavaScript
Vue的非父子组件之间传值
全局事件总线 一种组件间通信的方式,适用于任意组件间通信
|
缓存 JavaScript 前端开发
Vue Props、Slot、v-once、非父子组件间的传值....
Vue Props、Slot、v-once、非父子组件间的传值....
86 0