- 前言
- 简介
keep-alive是vue内置的抽象组件(组件自身不会渲染成真实的dom元素),其主要的作用是将需要缓存的组件保存在内存中,下次需要用到该组件时直接从缓存中获取,从而减少了重新渲染组件的开销。
- 使用场景举例
从菜单下的列表页(userPage)跳转到新增页(addUserPage),要求从addUserPage回到到userPage时记住用户之前的列表筛选条件,同时请求最新的列表数据。
使用keep-alive即可轻松实现,而无需自己编程来实现保存列表筛选条件的处理逻辑。// /src/views/menu3/index.vue <template> <keep-alive include="UserPage"> <router-view /> </keep-alive> </template> // /src/router/index.js 配置上述页面路由关系 { path: "/user", component: (resolve) => { return require(["@/views/menu3"], resolve); }, //嵌套子路由 children: [ { path: "/user/page", name: "UserPage", component: (resolve) => { return require(["@/views/menu3/page1/index.vue"], resolve); }, }, { path: "/user/add", component: (resolve) => { return require(["@/views/menu3/page1/add.vue"], resolve); }, } ] }, // 列表页(UserPage) export default{ data(){ return{}; }, created(){}, activated(){ // 发送ajax请求获取最新列表数据 }, deactivated(){ console.log("UserPage deactivated"); } };本地演示视频无法上传,大概意思就是演示被缓存的组件可以记住筛选条件。
一直以来,自己只知道如何使用这个组件,但对于它的具体实现过程却一无所知。于是我下去参考了其源码,整理了自己的一些收获,如有不恰当之处还望各位大佬指正。
- 简介
- 具体实现
-
附上源码
// src/core/components/keep-alive.ts export default { name: 'keep-alive', abstract: true, // 为true代表抽象组件 props: { include: patternTypes, exclude: patternTypes, max: [String, Number] }, methods: { cacheVNode() { const { cache, keys, vnodeToCache, keyToCache } = this if (vnodeToCache) { const { tag, componentInstance, componentOptions } = vnodeToCache cache[keyToCache] = { name: _getComponentName(componentOptions), tag, componentInstance } keys.push(keyToCache) // prune oldest entry if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } this.vnodeToCache = null } } }, created() { this.cache = Object.create(null) this.keys = [] }, destroyed() { for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys) } }, mounted() { this.cacheVNode() this.$watch('include', val => { pruneCache(this, name => matches(val, name)) }) this.$watch('exclude', val => { pruneCache(this, name => !matches(val, name)) }) }, updated() { this.cacheVNode() }, render() { const slot = this.$slots.default console.log("keep-alive:",slot); const vnode = getFirstComponentChild(slot) const componentOptions = vnode && vnode.componentOptions if (componentOptions) { // check pattern const name = _getComponentName(componentOptions) const { include, exclude } = this if ( // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode } const { cache, keys } = this 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 if (cache[key]) { vnode.componentInstance = cache[key].componentInstance // make current key freshest remove(keys, key) keys.push(key) } else { // delay setting the cache until update this.vnodeToCache = vnode this.keyToCache = key } // @ts-expect-error can vnode.data can be undefined vnode.data.keepAlive = true } return vnode || (slot && slot[0]) } }与其他的内置组件不同的是,keep-alive组件没有<template>标签,所以其自身不会渲染成一个dom节点。
-
代码拆分
.... props: { include: patternTypes, exclude: patternTypes, max: [String, Number] }, .... // const patternTypes: Array<Function> = [String, RegExp, Array]props对象声明3个属性,其中include代表需要被缓存的组件们、exclude代表不期望被缓存的组件们、max声明可最大缓存的组件数量。patternTypes约束了include和exclude字段值的类型。
created() { this.cache = Object.create(null) this.keys = [] }, // 这里似乎用map更方便一点created钩子函数内声明了2个变量,keys变量用于存放cache对象中的所有的key,cache这个对象用于储存组件实例,以key:value的形式进行储存,其中value的类型声明如下
type CacheEntry = { name?: string tag?: string componentInstance?: Component } type CacheEntryMap = Record<string, CacheEntry | null> const cache:CacheEntryMap;组件内很重要的render函数会根据当前组件和缓存中的组件来决定最终要渲染的组件。当keep-alive实例缓存的组件被激活时会触发render函数。
// 获取keep-alive实例的默认插槽(数组),即放在<keep-alive>标签内部的组件 const slot = this.$slots.default // 取出第一个子节点(keep-alive只会渲染一个组件) const vnode = getFirstComponentChild(slot) // 获取组件实例化时传入的options,即.vue组件export default默认导出的配置对象 const componentOptions = vnode && vnode.componentOptions通过上面的三行代码我们已经获得了keep-alive组件的第一个子节点的,当配置项componentOptions不为空时,我们得判断这个组件是否需要缓存。
const name = _getComponentName(componentOptions) const { include, exclude } = this if ( // 组件不在期望缓存的组件列表内 (include && (!name || !matches(include, name))) || //组件在不期望被缓存的组件列表内 (exclude && name && matches(exclude, name)) ) { // 不进行缓存 return vnode } // _getComponentName function _getComponentName(opts?: VNodeComponentOptions): string | null { // 优先获取配置项options的name属性值,其次是标签名 return opts && (getComponentName(opts.Ctor.options as any) || opts.tag) }上面的代码描述了组件的标志(name或tag)匹配到实例exclude中的元素,此时直接返回该虚拟dom。
const { cache, keys } = this 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 if (cache[key]) { vnode.componentInstance = cache[key].componentInstance // LRU缓存策略 remove(keys, key) keys.push(key) }else { // 未命中缓存 // 保存组件vnode和key 等到触发update钩子时再缓存 this.vnodeToCache = vnode this.keyToCache = key }首先获取组件的key,如果cache中key对应的value值不为空,那么代表已经命中了缓存,这时我们只需要取出key值对应的componentInstance,因为keep-alive有规定最大缓存数,当缓存的组件个数大于等于max时,借助LRU算法的思想删掉那些最近很少访问的组件实例,所以当组件命中了缓存策略之后,需要更新一下该组件的key在keep-alive的keys数组的里面的下标。
下图是LRU策略思想的简图。

cacheVNode方法用来缓存组件
methods: { cacheVNode() { const { cache, keys, vnodeToCache, keyToCache } = this if (vnodeToCache) { const { tag, componentInstance, componentOptions } = vnodeToCache cache[keyToCache] = { name: _getComponentName(componentOptions), tag, componentInstance } keys.push(keyToCache) // LRU缓存策略 if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } // 重置keep-alive实例的vnodeToCache this.vnodeToCache = null } } },从keep-alive实例上获取到需要缓存的组件的vnode,从vnode中我们可以拿到一些关于虚拟节点vnode的属性,然后我们更新cache中key属性的value值,同时将组件的key放进keys数组中,若缓存栈已满则需要删除keys数组中第一个key(LRU缓存策略思想)。
mounted() { this.cacheVNode();// 缓存组件 this.$watch('include', val => { pruneCache(this, name => matches(val, name)) // 判断组件是否期望被缓存 }) this.$watch('exclude', val => { pruneCache(this, name => !matches(val, name))// 判断组件是否不应该被缓存 }) },mounted钩子里面调用了cacheVNode方法进行缓存组件实例,同时监听了keep-alive实例的include和exclude属性值(即缓存规则)的变化,若传入的值发生变化则调用pruneCache方法。
function pruneCache( keepAliveInstance: { cache: CacheEntryMap; keys: string[]; _vnode: VNode }, filter: Function ) { const { cache, keys, _vnode } = keepAliveInstance for (const key in cache) { const entry = cache[key] if (entry) { const name = entry.name if (name && !filter(name)) { // 注意这里有取反操作符!,删除不满足缓存规则的组件 pruneCacheEntry(cache, key, keys, _vnode) } } } } function pruneCacheEntry( cache: CacheEntryMap, key: string, keys: Array<string>, current?: VNode //current代表当前处于活跃状态的组件 ) { const entry = cache[key] // 判断entry代表的组件是否是当前处于渲染状态的组件 if (entry && (!current || entry.tag !== current.tag)) { // @ts-expect-error can be undefined entry.componentInstance.$destroy()// 调用vue内置组件Component实例的销毁方法 } cache[key] = null remove(keys, key) }在该函数内对keep-alive的cache对象进行遍历,取出各组件的key值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry方法将这个已经不需要缓存的组件实例先销毁掉,然后再将其从cache、keys中删除。
updated() { this.cacheVNode() },当keep-alive缓存的组件实例状态发生变化时(即触发其activated或deactivated钩子时),keep-alive会重新执行cacheVNode方法执行缓存逻辑。
destroyed() { for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys) } },destroyed钩子函数会在 keep-alive实例被销毁时触发,会遍历cache中存储的组件实例,和当前处于渲染状态的组件进行对比,删除处于非活跃状态的缓存实例。
-
浙公网安备 33010602011771号