<!-- 非活跃的组件将会被缓存! -->
<keep-alive>
<component :is="activeComponent" />
</keep-alive>
1,是一个内部组件,用于缓存组件实例。在组件切换时,不用重新创建而是使用缓存中的组件实例。
2,有3个属性:
include
和 exclude
属性,可以控制哪些组件进入缓存。max
属性可以设置最大缓存数,当缓存的实例超过该数时,vue会移除最久没有使用的组件缓存。<!-- 以英文逗号分隔的字符串 -->
<keep-alive include="Comp1,Comp2">
<component :is="view" />
</keep-alive>
<!-- 数组 -->
<keep-alive :include="['Comp1', 'Comp2']">
<component :is="view" />
</keep-alive>
3,受 keep-alive
影响,内部所有嵌套的组件都有2个生命周期函数。activated
和 deactivated
。第1次 activated
是在 mounted
之后。
4,当嵌套组件是 <router-view>
时,缓存的是路由对应的组件。
因为
<router-view>
是函数式组件,只有一个目的,生成虚拟DOM。它没有状态,不生成实例。有穿透的效果。
<keep-alive>
<router-view></router-view>
</keep-alive>
keep-alive
在内部维护了一个缓存对象和 keys
数组:
// keep-alive 组件的生命周期函数
created () {
this.cache = Object.create(null)
this.keys = []
}
keys
数组记录当前缓存组件的 key
,如果组件没有指定,则会自动为组件生成唯一的 key
。
cache
对象以 key
为键,vnode 为值,来缓存组件对应的虚拟 DOM。
在 keep-alive
组件的 render
函数中,大致逻辑:
判断当前渲染的 vnode 是否有对应的缓存,有则从缓存中读取对应组件实例,没有则将其缓存。当缓存数量 > max 时,会移除掉 keys
数组的第1个元素。
// 更新逻辑其实在 update 函数中,这里结合在一起容易理解。
render(){
const slot = this.$slots.default; // 获取默认插槽
const vnode = getFirstComponentChild(slot); // 得到插槽中的第一个组件的 vnode
const name = getComponentName(vnode.componentOptions); // 获取组件名字
const { cache, keys } = this; // 获取当前的缓存对象和key数组
const key = ...; // 获取组件的key值,若没有,会按照规则自动生成
if (cache[key]) {
// 有缓存
// 关键:重用组件实例
vnode.componentInstance = cache[key].componentInstance
remove(keys, key); // 删除key
// 将 key加入到数组末尾,这样是为了保证最近使用的组件在数组中靠后,反之靠前
keys.push(key);
} else {
// 无缓存,则进行缓存
cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
// 超过最大缓存数量,移除第一个key对应的缓存
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
return vnode || (slot && slot[0])
}
注意到,render()
返回的就是插槽中的第一个组件的 vnode。所以页面上显示的也就是这个子组件。
在后台管理系统中比较常见,比如有2种情况就会用到:
这2种情况,都可以做成 tab 选项卡。
首先维护一个【tab选项卡】的状态,比如放到 Vuex 中。不放到组件内部,是因为不好使用,因为可能不止侧边栏会添加 tab,其他地方可能也会添加页面到 tab 选项卡。
export default new Vuex.Store({
modules: {
tabs: {
namespaced: true,
state: {
pageNames: [] // 选项卡的页面
},
mutations: {
addPage(state, newPageName) {
if (!state.pageNames.includes(newPageName)) {
state.pageNames.push(newPageName)
}
},
removePage(state, pageName) {
const index = state.pageNames.indexOf(pageName)
if (index >= 0) {
state.pageNames.splice(index, 1)
}
}
}
}
}
})
其他的看代码就一目了然了:(省略了CSS)
<template>
<div>
<!-- 固定在页面左侧 -->
<div>
<h1>侧边栏</h1>
<ul>
<router-link
v-for="item in $router.options.routes"
:key="item.path"
tag="li"
:to="{ name: item.name }"
active-class="blue"
>
<span>{{ item.name }}</span>
<button @click="handleAddPage(item.name)">+</button>
</router-link>
</ul>
</div>
<!-- 页面内容区域 -->
<div>
<!-- 固定在页面顶部,左侧和侧边栏对齐 -->
<div v-if="$store.state.tabs.pageNames.length">
<span>tab选项卡:</span>
<ul>
<router-link
v-for="pageName in $store.state.tabs.pageNames"
:key="pageName"
tag="li"
active-class="green"
:to="{ name: pageName }"
>
<span>{{ pageName }}</span>
<button @click="handleRemovePage(pageName)">x</button>
</router-link>
</ul>
</div>
<!-- 页面内容展示区域 -->
<keep-alive :include="$store.state.tabs.pageNames">
<router-view></router-view>
</keep-alive>
</div>
</div>
</template>
<script>
export default {
methods: {
handleAddPage(pageName) {
this.$store.commit('tabs/addPage', pageName)
},
handleRemovePage(pageName) {
this.$store.commit('tabs/removePage', pageName)
}
}
}
</script>
以上。