keep-alive原理及实践

110 阅读2分钟

啥玩意

kee-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染 。也就是所谓的组件缓存

用法及参数

<keep-alive>
    <component />
</keep-alive>

<!-- vue3 -->
<router-view v-slot="{ Component }">
  <keep-alive>
    <component
      :is="Component"
    />
  </keep-alive>
</router-view>

参数:

  • include包含的组件(类型:字符串,数组,正则表达式;只有匹配的组件会被缓存)
  • exclude排除的组件(类型:字符串,数组,正则表达式;任何匹配的组件都不会被缓存,优先级高于include)
  • max缓存组件的最大值(类型:字符,数字;可以控制缓存组件的个数,超过会删除最开始缓存的)

include和exclude参数匹配的是组件name字段,并不是路由name

钩子函数

  • activated 当 keepalive 包含的组件再次渲染的时候触发
  • deactivated 当 keepalive 包含的组件销毁的时候触发

执行时机

  1. beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开。
  2. beforeEach: 路由全局前置守卫,可用于登录验证、全局路由loading等,router实例上绑定的
  3. beforeEnter: 路由独享守卫,routerlist上定义的
  4. beforeRouteEnter: 路由组件的组件进入路由前钩子,vue文件内部定义的
  5. beforeResolve:路由全局解析守卫
  6. afterEach:路由全局后置钩子
  7. beforeCreate:组件生命周期,不能访问this。
  8. created:组件生命周期,可以访问this,不能访问dom。
  9. beforeMount:组件生命周期
  10. deactivated: 离开缓存组件a,或者触发a的beforeDestroy和destroyed组件销毁钩子。
  11. mounted:访问/操作dom。
  12. activated:进入缓存组件,进入a的嵌套子组件(如果有的话)。
  13. 执行beforeRouteEnter回调函数next。

非最佳实践

光伏落地

<keep-alive
  ref="keepAlive"
  :include="cachedViews"
>
  <router-view
    v-if="keeplivePathList2.includes($route.path)"
    :key="$route.path"
  />
</keep-alive>
<router-view
  v-if="!keeplivePathList2.includes($route.path)"
  :key="$route.path"
/>

export default {
  name: 'AppMain',
  data() {
    return {
      keeplivePathList2: [
        '/busManage/getMaterials/list',
        '/busManage/customerList',
        '/internal/staffManage/list',
        '/busManage/site_survey/list',
        ...
      ],
    }
  },
  computed: {
    cachedViews() {
      // .map((item) => item.replace(///g, ''))
      return this.$store.state.tagsView.cachedViews
    },
  },
  created() {
  },
}

通过v-if方式来确认哪些页面需要增加缓存。PS:这里最好的解决方式是通过在router列表的meta上面加上缓存标签,我们这里router路由在后台管理,后台说是不太好改结构,所以缓存页面在前端写死了。

改造后存在的问题:
1.增加缓存的页面第二次不会执行created钩子函数,所以如果我们在详情页修改了当前数据状态,返回列表时,数据也不会刷新,这里我们

解决办法:将created钩子的执行内容全部移入到activated钩子里,保证列表数据是最新且其他查询、分页参数缓存。

2.关闭当前tags,再次打开页面时,页面仍然缓存关闭之前状态,明显不符合使用习惯。
解决办法:通过include参数来控制,只对当前tags时打开的页面进行缓存,关闭当前tag时,此页面缓存清除,这里一定要注意我们这里绑定的cachedViews路由的path所以我们需要将页面组件的name名称替换成当前页面的路由

光伏新增缓存路由步骤

1.在AppMain.vue页面keeplivePathList2增加需要缓存的路由path

2.将当前路由对应的vue文件的name字段修改成path路径

c端做法

最终实现:home->list 不缓存list
detail->list 缓存list

meta 属性

<keep-alive>
     // 这个v-if一定写在router-view上 不要写在keep-alive上
    <router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
export default new Router({
  routes: [
    {
      path: '/',
      name: 'home',
      component: home,
      meta: {
        keepAlive: false // 不需要缓存
      }
    },
    {
      path: '/list',
      name: 'list',
      component: list,
      meta: {
        keepAlive: true // 需要被缓存
      }
    },
    {
      path: '/detail',
      name: 'detail',
      component: detail,
      meta: {
        keepAlive: false // 不需要被缓存
      }
    },
  ]
})

问题:home->list 也会被缓存

解决办法

home组件

export default {
    data() {
        return {};
    },
    methods: {},
    beforeRouteLeave(to, from, next) {
         // 设置下一个路由的 meta
        if(to.name === 'list') {
          to.meta.keepAlive = false;  // 让 list 不缓存,即刷新
        }
        next();
    }
};

detail组件

export default {
        data() {
            return {};
        },
        methods: {},
        beforeRouteLeave(to, from, next) {
            // 设置下一个路由的 meta
            if(to.name === 'list') {
              to.meta.keepAlive = true; // 让 list 缓存,即不刷新
            }
            next();
        }
};

总结:在跳转前的组件里的beforeRouteLeave钩子中操作to.meta.keepAlive来控制页面是否缓存

用 include (exclude例子类似)

<keep-alive :include="$store.state.keepAlive.list">
    <router-view />
</keep-alive>
const state = {
    list: []
}

const mutations = {
    add(state, name) {
        state.list.indexOf(name) < 0 && state.list.push(name)
    },
    remove(state, name) {
        state.list = state.list.filter(v => {
            return v != name
        })
    },
    clean(state) {
        state.list = []
    }
}

export default {
    namespaced: true,
    state,
    mutations
}

list组件

beforeRouteEnter(to, from, next) {
    next(vm => {
        vm.$store.commit('keepAlive/add', 'List')
    })
},
// 页面离开前
beforeRouteLeave(to, from, next) {
    // 不是去deital页面就移除
    if (['detail'].indexOf(to.name) < 0) {
        this.$store.commit('keepAlive/remove', 'List')
    }
    next()
}

源码解析

位置: src/core/components/keep-alive.js

export default {
  name: 'keep-alive',
  abstract: true,

  props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: [String, Number]
  },

  created () {
    this.cache = Object.create(null)
    this.keys = []
  },

  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render() {
    /* 获取默认插槽中的第一个组件节点 */
    const slot = this.$slots.default
    const vnode = getFirstComponentChild(slot)
    /* 获取该组件节点的componentOptions */
    const componentOptions = vnode && vnode.componentOptions

    if (componentOptions) {
      /* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
      const name = getComponentName(componentOptions)

      const { include, exclude } = this
      /* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
      if (
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      /* 获取组件的key值 */
      const key = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
     /*  拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存 */
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      }
        /* 如果没有命中缓存,则将其设置进缓存 */
        else {
        cache[key] = vnode
        keys.push(key)
        // prune oldest entry
        /* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}