背景:在 B 端系统中,为了使用方便咱们会在页面设计中加上标签页相似浏览器上方标签页的功能,为了使用体验更加接近浏览器标签页,咱们须要针对路由进行缓存。本文主要介绍 Vue 项目针对不一样业务场景如何利用 keep-alive 来实现标签页动态缓存。html
keep-alive 是一个抽象组件,不会和子组件创建父子关系,也不会做为节点渲染到页面上。vue
关于抽象组件 Vue 的文档没有提这个概念,它有一个属性 abstract 为 true,在抽象组件的生命周期过程当中,咱们能够对包裹的子组件监听的事件进行拦截,也能够对子组件进行 Dom 操做,从而能够对咱们须要的功能进行封装,而不须要关心子组件的具体实现。除了kepp-alive还有<transition><transition-group>等。node
keep-alive 的模式下多了 activated 这个生命周期函数, keep-alive 的声明周期执行:算法
created-> mounted-> activated,退出时触发 deactivated 当再次进入(前进或者后退)时,只触发 activated。vuex
keep-alive 是由 render 函数决定渲染结果,在开头会获取插槽内的子元素,调用 getFirstComponentChild 获取到第一个子元素的 VNode。后端
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
复制代码
接着判断当前组件是否符合缓存条件,组件名与 include 不匹配或与 exclude 匹配都会直接退出并返回 VNode,不走缓存机制。api
// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
复制代码
匹配条件经过会进入缓存机制的逻辑,若是命中缓存,从 cache 中获取缓存的实例设置到当前的组件上,并调整 key 的位置将其放到最后(LRU 策略)。 若是没命中缓存,将当前 VNode 缓存起来,并加入当前组件的 key。若是缓存组件的数量超出 max 的值,即缓存空间不足,则调用 pruneCacheEntry 将最旧的组件从缓存中删除,即 keys[0] 的组件。以后将组件的 keepAlive 标记为 true,表示它是被缓存的组件。数组
LRU 缓存策略:从内存中找出最久未使用的数据置换新的数据.算法根据数据的历史访问记录来进行淘汰数据,其核心思想是若是数据最近被访问过,那么未来被访问的概率也更高。浏览器
const { cache, keys } = this
const key: ?string = 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
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
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
复制代码
pruneCacheEntry 负责将组件从缓存中删除,它会调用组件 $destroy 方法销毁组件实例,缓存组件置空,并移除对应的 key。缓存
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
复制代码
通常采用在 router 的 meta 属性里增长一个 keepAlive 字段,而后在父组件或者根组件中,根据 keepAlive 字段的状态使用 keep-alive 标签,实现对路由的缓存:
<keep-alive>
<router-view v-if="$route.meta.keepAlive" />
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" />
复制代码
使用 vuex 配合 exclude 和 include,经过 include 和 exclude 决定那些组件进行缓存。注意这里说的是组件,而且 cachedView 数组存放的是组件的名字,以下:
<keep-alive :include="$store.state.keepAlive.cachedView">
<router-view></router-view>
</keep-alive>
复制代码
在 SPA 应用中用户但愿在 Tab 多个页面来回切换的时候,不要丢失查询的结果,关闭后清除缓存。
以下图:
指望是用户在切换 Tab 时 页面时缓存的,当用户关闭 Tab ,从新从左侧菜单打开时是不缓存。
这样是持久缓存了整个页面,问题也就出现当用户经过 Tab 关闭页面而后从左侧打开菜单时是缓存的页面,这个不符合平常使用习惯,因此为了解决数据新鲜度的问题能够在 activated 触发查询请求就能保证数据的新鲜度。
activated(){
getData()
}
复制代码
可是使用后发现因为你切换 Tab 时每次都会请求数据,可是若是项目的数据量有很大频繁请求会给后端形成很大压力 。
版本一须要频繁拉去数据致使此方案已不合适只能动态缓存组件方案。
<keep-alive :include="cachedViews">
<router-view :key="key"></router-view>
</keep-alive>
复制代码
其中 cachedViews 是经过监听路由动态增长删除维护要缓存的组件名称(因此组件名称不要重复)数组:
const state = {
cachedViews: [],
}
const mutations = {
ADD_VIEWS: (state, view) => {
if (state.cachedViews.includes(view.name)) return
state.cachedViews.push(view.name)
},
DEL_CACHED_VIEW: (state, view) => {
const index = state.cachedViews.indexOf(view.name)
index > -1 && state.cachedViews.splice(index, 1)
},
}
const actions = {
addCachedView({ commit }, view) {
commit('ADD_VIEWS', view)
},
deleteCachedView({ commit }, view) {
commit('DEL_CACHED_VIEW', view)
},
}
export default {
namespaced: true,
state,
mutations,
actions,
}
复制代码
经过监听路由变化:
watch: {
'$route'(newRoute) {
const { name } = newRoute
const cacheRout = this.ISCACHE_MAP[name] || []
cacheRout.map((item) => {
store.dispatch('cached/addCachedView', { name: item })
})
},
},
当经过 Tab 关闭页面时清除组件名称:
closeTag(newRoute) {
const { name } = newRoute
const cacheRout = this.ISCACHE_MAP[name] || []
cacheRout.map((item) => {
store.dispatch('cached/deleteCachedView', { name: item })
})
}
复制代码
可是在遇到嵌套路由时在层级不一样的 router-view 中切换 Tab 会出现缓存数据失效的问题,没法缓存组件,嵌套路由以下:
经过维护两套数据,一套嵌套给左侧菜单,一套扁平化后注册路由,改造后的路由:
经过上面 keep-alive 解析能够知道,keep-alive就是把经过 include 匹配的组件的 vnode,放到 keep-alive 组件的一个 cache 对象中,下次渲染时,若是能在这里面找到,就直接渲染vnode。因此把这个 cache 对象,放到全局去(全局变量或者 vuex),这样我就能够不用缓存 ParnetView 也能缓存其指定的子组件了。
import Vue from 'vue'
const cache = {}
const keys = []
export const removeCacheByName = (name) => {/* 省略移除代码 */}
export default Object.assign({}, Vue.options.components.KeepAlive, {
name: 'NewKeepAlive',
created() {
this.cache = cache
this.keys = keys
},
destroyed() {},
})
复制代码
从上文能够知道 keep-alive 是从 cache 中获取缓存的实例设置到当前的组件上,key 是组件的名称,能够经过改造 getComponentName 方法,组件名称获取更改成路由名称使其缓存的映射关系只与 route name 值有关系。
function getComponentName(opts) {
return this.$route.name
}
复制代码
cache 缓存 key 也更改成路由名称。
文|揣歪
关注得物技术,携手走向技术的云端