【Vue实用组件】组件缓存块 (keep-alive + transition)

10 篇文章 0 订阅
4 篇文章 0 订阅

组件缓存块

  • 简述: 简化版 keep-alive + transition,仅使用 key 定义需要缓存的组件,并且可以加入过渡效果。
  • 使用场景: 需要缓存指定切换显隐的组件(带过渡效果),且组件中不存在 name 值的情况,可根据指定 key 值设置需要缓存的组件(如路由渲染等场景)。

组件源码 (Vue2)

<script>
/**
 *  组件缓存块
 *  @description  简化版 keep-alive + transition,仅使用 key 定义需要缓存的组件
 *  @param  {String|Number} max         组件缓存最大数量
 *  @param  {Array[String]} keys        需缓存组件的 key
 *  @param  {Object}        transition  组件过渡配置 (可绑定 transition 的钩子)
 *  @property {Array}       cache       缓存序列 { key, componentInstance }
 *  @property {Function}    clearCache  清除所有已缓存的组件
 *  @property {Function}    pruneCache  清除指定 key 值的缓存组件
 */
export default {
  name: 'AliveBlock',
  abstract: true,
  props: {
    max: [String, Number],
    keys: [Array],
    transition: [Object]
  },
  watch: {
    keys(val) {
      const cache = this.cache
      const keys = val || []
      // 过滤已移除的缓存组件
      for (let i = 0; i < cache.length; i++) {
        if (keys.includes(cache[i].key)) continue
        cache[i].componentInstance.$destroy()
        cache.splice(i--, 1)
      }
    }
  },
  methods: {
    // 更新缓存组件
    updateCache() {
      const { aliveKey: key, componentInstance } = this.nextCache || {}
      // 存在待更新的实例时可进行更新操作 (延迟更新时可能不存在实例)
      if (key != null && componentInstance) {
        const cache = this.cache
        const sameCache = cache.find(item => item.key === key)
        if (sameCache) {
          // 若存在相同 key 值的实例则替换
          if (sameCache.componentInstance !== componentInstance) {
            sameCache.componentInstance.$destroy()
            sameCache.componentInstance = componentInstance
          }
        } else {
          // 未缓存时添加新的缓存项
          cache.push({ key, componentInstance })
          // 校验数量限制,若超出限制则销毁最早活动的实例
          if (this.max && cache.length > parseInt(this.max)) {
            cache[0].componentInstance.$destroy()
            cache.splice(0, 1)
          }
        }
        this.nextCache = null
      }
    },
    // 清空缓存组件 (内置方法,可通过 ref 当前实例使用)
    clearCache() {
      const cache = this.cache
      cache.forEach(item => item.componentInstance.$destroy())
      cache.splice(0, cache.length)
    },
    // 清除指定 key 值的缓存组件 (内置方法,可通过 ref 当前实例使用)
    pruneCache(cacheKey) {
      const cache = this.cache
      const cacheIndex = cacheKey != null ? cache.findIndex(item => item.key === cacheKey) : -1
      if (cacheIndex == -1) return
      cache[cacheIndex].componentInstance.$destroy()
      cache.splice(cacheIndex, 1)
    }
  },
  created() {
    // 初始化缓存序列
    this.cache = []
  },
  mounted() {
    // 首次载入后更新缓存
    this.updateCache()
  },
  updated() {
    // 每次组件更新后更新缓存 (含 transition 时不生效,需通过钩子延迟更新)
    this.updateCache()
  },
  destroyed() {
    // 销毁实例时清空缓存
    this.clearCache()
  },
  render(h) {
    // 仅取第一个子节点
    const [vnode] = this.$slots.default || []
    if (vnode) {
      // 仅节点存在 key 值并包含于 keys 可缓存组中才可以进行缓存
      if (vnode.key != null && this.keys && this.keys.includes(vnode.key)) {
        const cache = this.cache
        const cacheIndex = cache.findIndex(item => item.key === vnode.key)
        // 设置独有的 key 属性,因 VNode 节点中的 key 值在渲染时可能会发生改变 (如插入到 transition 节点后),所以需要多处理一层
        vnode.aliveKey = vnode.key
        // 当前节点设为 keep-alive 模式,此步骤可触发 activated, deactivated 组件钩子
        vnode.data.keepAlive = true
        // 当缓存序列中存在已缓存过的项时,覆盖子节点原有的组件实例 (覆盖后渲染时不创建新的实例)
        if (cacheIndex != -1) {
          vnode.componentInstance = cache[cacheIndex].componentInstance
          // 子节点触发活性,排序当前缓存到末尾处 (排序缓存先后顺序,含最大数量限制时避免当前缓存在首位时被清除)
          cache.push(...cache.splice(cacheIndex, 1))
        }
        // 子节点未被缓存时设置下次延迟更新的缓存 (组件实例在渲染后生成),否则清空原来的待更新缓存
        this.nextCache = cacheIndex != -1 ? null : vnode
      }
      // 是否使用过渡
      if (this.transition) {
        const props = { mode: 'out-in', ...this.transition }
        const on = { ...this.$listeners }
        // 存在待更新缓存时,通过 before-enter 钩子在组件实例生成后更新缓存
        this.nextCache && (on['before-enter'] = (...args) => {
          this.updateCache()
          const fn = this.$listeners['before-enter']
          typeof fn == 'function' && fn(...args)
        })
        return h('transition', { props, on }, [vnode])
      }
    }
    return vnode
  }
}
</script>

使用示例 1 (基本使用)

  • index.vue (父组件)
<template>
  <div>
    <div>
      <button v-for="key in keys" :key="key" @click="name = key">{{ key }}</button>
    </div>
    <alive-block :keys="keys" :transition="{ name: 'fade' }">
      <component :is="name" :key="name" />
    </alive-block>
  </div>
</template>

<script>
import AliveBlock from '@/components/AliveBlock'
import c1 from './c1'
import c2 from './c2'
import c3 from './c3'

export default {
  components: { AliveBlock, c1, c2, c3 },
  data() {
    return {
      name: '',
      keys: ['c1', 'c2', 'c3']
    }
  }
}
</script>

<style>
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>
  • c1.vue (子组件 1)
<template>
  <div>
    <h3>Name: c1</h3>
    <div>Input: <input v-model="val" /></div>
  </div>
</template>

<script>
export default {
  name: 'c1',
  data() {
    return { val: '' }
  }
}
</script>
  • c2.vue (子组件 2)
<template>
  <div>
    <h3>Name: c2</h3>
    <div>Input: <input v-model="val" /></div>
  </div>
</template>

<script>
export default {
  data() {
    return { val: '' }
  }
}
</script>
  • c3.vue (子组件 3)
<template>
  <div>
    <h3>Name: c3</h3>
    <div>Input: <input v-model="val" /></div>
  </div>
</template>

<script>
export default {
  data() {
    return { val: '' }
  },
  created() { console.log('created') },
  mounted() { console.log('mounted') },
  activated() { console.log('activated') },
  deactivated() { console.log('deactivated') },
  destroyed() { console.log('destroyed') }
}
</script>

使用示例 2 (缓存路由渲染场景)

  • 示例代码
<template>
  <section>
    <alive-block :keys="keepAliveNames" :transition="{ name: 'fade', mode: 'out-in' }">
      <router-view :key="$route.name" />
    </alive-block>
  </section>
</template>

<script>
import AliveBlock from '@/components/AliveBlock'

export default {
  components: { AliveBlock },
  computed: {
    keepAliveNames() {
      // 全局缓存的路由 cachedViews[VueRoute: { ..., name, meta: {} }]
      return this.$store.state.cachedViews.filter(item => item.meta.keepAlive).map(item => item.name)
    }
  }
}
</script>

<style>
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.4s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>
  • 若以上 <router-view> 路由页面下内嵌了 <router-view>,则需要多包一层 <keep-alive>
<template>
  <keep-alive>
    <router-view />
  </keep-alive>
</template>
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值