keep-alive
面试题:请阐述keep-alive组件的作用和原理
keep-alive组件是vue的内置组件,用于缓存内部组件实例。这样做的目的在于,keep-alive内部的组件切回时,不用重新创建组件实例,而直接使用缓存中的实例,一方面能够避免创建组件带来的开销,另一方面可以保留组件的状态。
keep-alive具有include和exclude属性,通过它们可以控制哪些组件进入缓存。另外它还提供了max属性,通过它可以设置最大缓存数,当缓存的实例超过该数时,vue会移除最久没有使用的组件缓存。
受keep-alive的影响,其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是activated
和deactivated
,它们分别在组件激活和失活时触发。第一次activated
触发是在mounted
之后
在具体的实现上,keep-alive在内部维护了一个key数组和一个缓存对象
// keep-alive 内部的声明周期函数
created () {
this.cache = Object.create(null)
this.keys = []
}
key数组记录目前缓存的组件key值,如果组件没有指定key值,则会为其自动生成一个唯一的key值
cache对象以key值为键,vnode为值,用于缓存组件对应的虚拟DOM
在keep-alive的渲染函数中,其基本逻辑是判断当前渲染的vnode是否有对应的缓存,如果有,从缓存中读取到对应的组件实例;如果没有则将其缓存。
当缓存数量超过max数值时,keep-alive会移除掉key数组的第一个元素
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;
}
缺点:keep-alive的代价就是占用了更多的内存空间,当组件比较大时,里面可能可能会包含很多内容,这样会导致内存被占用,使用keep-alive相当于是用空间换取时间.
案例:一个很常见的场景,就是做后台管理系统的时候,可能某个页面有较多的需要输入的内容,当时当我们填写到一半的时候,离开了当前页面,去做了别的事情,再次回到该页面的时候,之前输入的内容都没了,这个时候我们的keep-alive就闪亮登场了,以下案例就是keep-alive的一个简单应用:
App.vue页面
<template>
<div id="app">
<!-- 左侧导航 -->
<div class="leftmenu">
<ul>
<li v-for="(menu,index) in meuns" :key="menu.path">
<router-link tag="span" :to="{name:menu.name}">{{menu.name}}</router-link>
<button @click="addPage(menu)">+</button>
</li>
</ul>
</div>
<div class="right-content">
<div>
<h2>选项卡</h2>
<ul style="display:flex">
<li v-for="(item,index) in activeTabs" :key="index">
<router-link tag="span" :to="{name:item}">{{item}}</router-link>
</li>
</ul>
</div>
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {};
},
mounted() {
console.log(this.$store.state.tabs.currentActiveTabNames, "router");
},
computed: {
meuns() {
return this.$router.options.routes || [];
},
activeTabs() {
return this.$store.state.tabs.currentActiveTabNames || [];
}
},
methods: {
addPage(menu) {
console.log(menu, "menu");
this.$store.commit("tabs/addPage", menu.name);
}
}
};
</script>
<style lang="scss">
li {
list-style: none;
}
ul {
margin: 0;
padding: 0;
}
.leftmenu {
height: 100vh;
width: 200px;
background-color: rgb(4, 29, 54);
li {
width: 100%;
line-height: 45px;
color: white;
text-align: center;
}
}
#app {
display: flex;
}
.right-content {
padding-left: 15px;
width: calc(100vw - 200px);
}
span {
padding: 0 5px;
}
</style>
Page1.vue
<template>
<div>
<h1>Page1</h1>
<input type="text">
<button @click="count++">+</button>
<span>{{count}}</span>
<button @click="count--">-</button>
</div>
</template>
<script>
export default {
name:"Page1",
data(){
return {
count:0
}
}
}
</script>
page2和page3的内容和page1是一样的,只不过将h1的内容改成了page2和page3,
router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Page1 from '../views/Page1.vue'
import Page2 from '../views/Page2.vue'
import Page3 from '../views/Page3.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/page1',
name: 'Page1',
component: Page1
},
{
path: '/page2',
name: 'Page2',
component: Page2
},
{
path: '/page3',
name: 'Page3',
component: Page3
},
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import tabs from './tabs.js'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
tabs
}
})
store/tabs.js
export default{
namespaced:true,
state:{
currentActiveTabNames:[]
},
mutations:{
addPage(state,newPageName){
if(!newPageName) return;
if(!state.currentActiveTabNames.includes(newPageName)){
state.currentActiveTabNames.push(newPageName)
}
},
removePage(state,pageName){
//找到在数组中的索引
let index=state.currentActiveTabNames.indexOf(pageName);
index>=0&&state.currentActiveTabNames.splice(index,1);
}
}
}
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
这样被保存在选项卡中的tab页就可以实现缓存的功能,比如下图中的page2和page3,当我们page3选项卡下输入了内容,并点击+号,把count增加到3,这时我们切换到其他页面,再回到page3的时候,之前输入的内容还是完好无损的保留在页面中.
image.png