定义

keep-alive是Vue中内置的一个抽象组件。它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

keep-alive是用来缓存组件的,比如我们有个列表页,在点击详情页之后,如果返回之后不想刷新列表页,就可以用keep-alive组件进行缓存。除此以外,还有很多应用场景。

用法

用法1:我们想要缓存某个组件,只要用keep-alive组件将其包裹就行。

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

用法2:包裹component组件缓存动态组件,或者包裹router-view缓存路由页面,也就是keep-alive配合路由守卫(元信息)实现缓存。

比如常在router.js路由表里定义好哪些页面需要缓存,就可以通过下面这样实现了:

{path: "/index",name: 'index',   component: () => import(/* webpackChunkName: "index" */ '@/pages/index'),meta: {title: '首页', keepAlive: true}
}
<keep-alive><router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive && isRouterAlive"></router-view>

属性

  • include - 逗号分隔字符串或正则表达式或一个数组来表示。只有名称匹配的组件会被缓存。
  • exclude - 逗号分隔字符串或正则表达式或一个数组来表示。任何名称匹配的组件都不会被缓存。
  • max - 数字。最多可以缓存多少组件实例。

include 和 exclude 属性允许组件有条件地缓存:

<!-- 逗号分隔字符串 -->
<keep-alive include="a,b"><component :is="view"></component>
</keep-alive><!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/"><component :is="view"></component>
</keep-alive><!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']"><component :is="view"></component>
</keep-alive>

注意:想要缓存的组件一定要给定name属性,并且要和include,exclude给定的值一致

生命周期钩子

keep-alive提供了两个生命钩子,分别是activateddeactivated

因为keep-alive会将组件保存在内存中,并不会销毁以及重新创建,所以不会重新调用组件的created等方法,需要用activated与deactivated这两个生命钩子来得知当前组件是否处于活动状态。

组件一旦被keep-alive缓存,那么再次渲染的时候就不会执行 created、mounted 等钩子函数。使用keep-alive组件后,被缓存的组件生命周期会多activated和deactivated 两个钩子函数,它们的执行时机分别是keep-alive包裹的组件激活时调用和停用时调用。

源码

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.defaultconst vnode = getFirstComponentChild(slot)/* 获取该组件节点的componentOptions */const componentOptions = vnode && vnode.componentOptionsif (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 } = thisconst 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.keyif (cache[key]) {vnode.componentInstance = cache[key].componentInstance// make current key freshestremove(keys, key)keys.push(key)} else {cache[key] = vnodekeys.push(key)// prune oldest entryif (this.max && keys.length > parseInt(this.max)) {pruneCacheEntry(cache, keys[0], keys, this._vnode)}}vnode.data.keepAlive = true}return vnode || (slot && slot[0])}
}

可以看到,它有3个属性,即有3个props。此外,它有createddestroyedmountedrender四个钩子。

原理

created和destroyed钩子

  • created钩子会创建一个cache对象,用来作为缓存容器,保存vnode节点。
  • destroyed钩子则在组件被销毁的时候清除cache缓存中的所有组件实例。
created () {/* 缓存对象 */this.cache = Object.create(null)this.keys = []
},
/* destroyed钩子中销毁所有cache中的组件实例 */
destroyed () {for (const key in this.cache) {pruneCacheEntry(this.cache, key, this.keys)}
},

render钩子

keep-alive实现缓存的核心代码就在这个钩子函数里。

  1. 先获取到插槽里的内容
  2. 调用getFirstComponentChild方法获取第一个子组件,获取到该组件的name,如果有name属性就用name,没有就用tag名。
/* 获取该组件节点的名称 */
const name = getComponentName(componentOptions)/* 优先获取组件的name字段,如果name不存在则获取组件的tag */
function getComponentName (opts: ?VNodeComponentOptions): ?string {return opts && (opts.Ctor.options.name || opts.tag)
}
  1. 接下来会将这个name通过include与exclude属性进行匹配,匹配不成功(说明不需要进行缓存)则不进行任何操作直接返回这个组件的 vnode(vnode是一个VNode类型的对象),否则的话走下一步缓存。匹配:
/* 检测name是否匹配 */
function matches (pattern: string | RegExp, name: string): boolean {if (typeof pattern === 'string') {/* 字符串情况,如a,b,c */return pattern.split(',').indexOf(name) > -1} else if (isRegExp(pattern)) {/* 正则 */return pattern.test(name)}/* istanbul ignore next */return false
}

检测include与exclude属性匹配的函数很简单,include与exclude属性支持字符串如"a,b,c"这样组件名以逗号隔开的情况以及正则表达式。matches通过这两种方式分别检测是否匹配当前组件。

const { include, exclude } = this
/* 如果name与include规则不匹配或者与exclude规则匹配则表示不缓存,直接返回vnode */
if ((include && (!name || !matches(include, name))) ||// excluded(exclude && name && matches(exclude, name))
) {return vnode
}
  1. 缓存机制:接下来的事情很简单,根据key在this.cache中查找,如果存在则说明之前已经缓存过了,直接将缓存的vnode的componentInstance(组件实例)覆盖到目前的vnode上面。否则将vnode存储在cache中。最后返回vnode(有缓存时该vnode的componentInstance已经被替换成缓存中的了)。缓存的处理:
/* 如果命中缓存,则直接从缓存中拿 vnode 的组件实例 */
if (cache[key]) {vnode.componentInstance = cache[key].componentInstance/* 调整该组件key的顺序,将其从原来的地方删掉并重新放在最后一个 */remove(keys, key)keys.push(key)
}
/* 如果没有命中缓存,则将其设置进缓存 */
else {cache[key] = vnodekeys.push(key)/* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */if (this.max && keys.length > parseInt(this.max)) {pruneCacheEntry(cache, keys[0], keys, this._vnode)}
}
/* 最后设置keepAlive标记位 */
vnode.data.keepAlive = true

命中缓存时会直接从缓存中拿 vnode 的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys中最后一个。

如果没有命中缓存,即该组件还没被缓存过,则以该组件的key为键,组件vnode为值,将其存入this.cache中,并且把key存入this.keys中。此时再判断this.keys中缓存组件的数量是否超过了设置的最大缓存数量值this.max,如果超过了,则把第一个缓存组件删掉。

为什么要删除第一个缓存组件并且为什么命中缓存了还要调整组件key的顺序?这其实应用了一个缓存淘汰策略LRU:
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

this.keys的逻辑:

  • 将新数据从尾部插入到this.keys中
  • 每当缓存命中(即缓存数据被访问),则将数据移到this.keys的尾部
  • 当this.keys满的时候,将头部的数据丢弃

mounted钩子

在这个钩子函数里,调用了pruneCache方法,以观测 include 和 exclude 的变化。

  mounted () {this.$watch('include', val => {pruneCache(this, name => matches(val, name))})this.$watch('exclude', val => {pruneCache(this, name => !matches(val, name))})},
watch: {/* 监视include以及exclude,在被修改的时候对cache进行修正 */include (val: string | RegExp) {pruneCache(this.cache, this._vnode, name => matches(val, name))},exclude (val: string | RegExp) {pruneCache(this.cache, this._vnode, name => !matches(val, name))}
},

如果include 或exclude 发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache函数,函数如下:

function pruneCache (keepAliveInstance, filter) {const { cache, keys, _vnode } = keepAliveInstancefor (const key in cache) {const cachedNode = cache[key]if (cachedNode) {const name = getComponentName(cachedNode.componentOptions)if (name && !filter(name)) {pruneCacheEntry(cache, key, keys, _vnode)}}}
}

在该函数内对this.cache对象进行遍历,取出每一项的name值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry函数将其从this.cache对象删除即可。

总结

Vue.js内部将DOM节点抽象成了一个个的VNode节点,keep-alive组件的缓存也是基于VNode节点的而不是直接存储DOM结构。它将满足条件(pruneCache与pruneCache)的组件在cache对象中缓存起来,在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染。

Vue中keep-alive原理相关推荐

  1. vue指令写在html中的原理,详解Vue中的MVVM原理和实现方法

    对Vue中的MVVM原理解析和实现首先你对Vue需要有一定的了解,知道MVVM.这样才能更有助于你顺利的完成下面原理的阅读学习和编写下面由我阿巴阿巴的详细走一遍Vue中MVVM原理的实现,这篇文章大家 ...

  2. [vue] 你知道vue中key的原理吗?说说你对它的理解

    [vue] 你知道vue中key的原理吗?说说你对它的理解 key的作用主要是为了高效的更新虚拟DOM; 如果没有唯一的key, 数据更新时, 相同节点更新前后无法准确一一对应起来,会导致更新效率降低 ...

  3. 前端面试 vue生命周期钩子是如何实现的?理解vue中模板编译原理?

    生命周期钩子在内部会被vue维护成一个数组(vue 内部有一个方法mergeOption)和全局的生命周期合并最终转换成数组,当执行到具体流程时会执行钩子(发布订阅模式),callHook来实现调用. ...

  4. [Vue源码] Vue中diff算法原理

    一. Vue中diff算法原理 理解: 1.先同级比较,在比较子节点 2.先判断一方有儿子一方没儿子的情况 3.比较都有儿子的情况 4.递归比较子节点 图: 1.原节点:ABCD,新节点:ABCDE, ...

  5. vue中的v-model原理,与组件自定义v-model

    VUE中的v-model可以实现双向绑定,但是原理是什么呢?往下看看吧 根据官方文档的解释,v-model其实是一个语法糖,它会自动的在元素或者组件上面解析为 :value="" ...

  6. c# mvvm模式获取当前窗口_对Vue中的MVVM原理解析和实现

    首先你对Vue需要有一定的了解,知道MVVM.这样才能更有助于你顺利的完成下面原理的阅读学习和编写 下面由我阿巴阿巴的详细走一遍Vue中MVVM原理的实现,这篇文章大家可以学习到: 1.Vue数据双向 ...

  7. vue 中的scoped原理

    scoped作用 在vue文件中的style标签上加上scoped属性,则style标签下的样式只能在本组件中使用. 如果每一个vue组件的style标签都加上了scoped,那就实现了样式的模块化. ...

  8. Vue 中 CSS 动画原理

    下面这段代码,是点击按钮实现hello world显示与隐藏 <div id="root"><div v-if="show">hello ...

  9. 通过几个问题深入分析Vue中的diff原理

    遇到的问题 在使用Vue渲染"可删减"的列表时,错误的使用index作为key,导致列表视图出现错乱. 点击查看问题 复现步骤:右侧有两行,在第一行的Input里输入1,在第二行I ...

  10. vue中实现双向数据绑定原理,使用了Object.defineproperty()方法,方法简单

    在vue中双向数据绑定原理,我们一般都是用v-model来实现的 ,但一般在面试话会问到其实现的原理, 方法比较简单,就是利用了es5中的一个方法.Object.defineproperty(),它有 ...

最新文章

  1. 【Java Web后台实验与开发】关于SSH框架的探索
  2. 每日两SQL(5),欢迎交流~
  3. mysql一次性获取几十万数据_《快速念咒——MySQL自学入门指南》:第2章——从表中获取更多数据(前言)...
  4. mysql设置停止二进制文件的操作_window下使用二进制文件对mysql数据库备份及恢复(再现用户操作)...
  5. 小程序 按需_小程序想要留住用户需哪些举措?
  6. hdu1257 最少拦截系统【想了好久】
  7. .Net Remoting中Remote Server的Port占用/释放问题
  8. 如何用Pygame写游戏(二十一)
  9. Asp.Net MVC 3【Filters(过滤器)】
  10. (libgdx学习)InputProcessor InputMultiplexer
  11. 获取当前时间getDate()注意点
  12. [转] Android SDK manager 无法获取更新版本列表
  13. 学材分析计算机一体化,计算机一体化课教案.doc
  14. 干法:经营者应该怎样工作
  15. 别@微信官方了,最全的圣诞帽都在这儿了!
  16. Java利用数组求某年某日某月是某年的第几天(数组)
  17. passwd_pro
  18. mac移动鼠标光标会变大_如何在Mac上使鼠标光标变大或变小
  19. CC00069.CloudKubernetes——|KuberNetes二进制部署.V22|3台Server|——|TLS_Bootstrapping|自动颁发证书|
  20. 【校招VIP】出品:在线实习“职查查”大V信息认证实战

热门文章

  1. 一个简单的2022春节倒计时
  2. [网络安全学习篇50]:Web架构安全分析
  3. 【Practical】随机过程
  4. 新版SQLite源码下载与编译
  5. 手机怎么给视频去除水印
  6. 傲梅分区助手坑我哭了很久
  7. 杂谈:使用SteamCMD搭建七日杀(7 days to die, 7DTD)Linux版本专用服务器
  8. 使用搜索引擎(百度)的技巧
  9. python实现AdaBoost算法
  10. java websocket心跳包_给websocket加入心跳包防止自动断开连接