「Vue源码学习」简单讲一讲keep-alive的原理吧
前言
大家好,我是林三心,用最通俗易懂讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。
- 「Vue源码学习(一)」你不知道的-数据响应式原理
- Vue源码学习(二)」你不知道的-模板编译原理
- 「Vue源码学习(三)」你不知道的-初次渲染原理
- 「Vue源码学习(四)」立志写一篇人人都看的懂的computed,watch原理
- 「Vue源码学习(五)」面试官喜欢问的——Vue常用方法源码解析
- Vue源码学习」你想知道Vuex的实现原理吗?
- 「Vue源码学习」你真的知道插槽Slot是怎么“插”的吗
- 15张图,20分钟吃透Diff算法核心原理,我说的!!!
- 林三心画了8张图,最通俗易懂的Vue3响应式核心原理解析
- 7张图,从零实现一个简易版Vue-Router,太通俗易懂了!
今天,就给大家讲讲Vue中常用的组件keep-alive的基本原理吧!
场景
可能大家在平时的开发中会经常遇到这样的场景:有一个可以进行筛选的列表页List.vue,点击某一项时进入相应的详情页面,等到你从详情页返回List.vue时,发现列表页居然刷新了!刚刚的筛选条件都没了!!!

keep-alive
是什么?
keep-alive是一个Vue全局组件keep-alive本身不会渲染出来,也不会出现在父组件链中keep-alive包裹动态组件时,会缓存不活动的组件,而不是销毁它们
怎么用?
keep-alive接收三个参数:
include:可传字符串、正则表达式、数组,名称匹配成功的组件会被缓存exclude:可传字符串、正则表达式、数组,名称匹配成功的组件不会被缓存max:可传数字,限制缓存组件的最大数量
include和exclude,传数组情况居多
动态组件
<keep-alive :include="allowList" :exclude="noAllowList" :max="amount">
<component :is="currentComponent"></component>
</keep-alive>
路由组件
<keep-alive :include="allowList" :exclude="noAllowList" :max="amount">
<router-view></router-view>
</keep-alive>
源码
组件基础
前面说了,keep-alive是一个Vue全局组件,他接收三个参数:
include:可传字符串、正则表达式、数组,名称匹配成功的组件会被缓存exclude:可传字符串、正则表达式、数组,名称匹配成功的组件不会被缓存max:可传数字,限制缓存组件的最大数量,超过max则按照LRU算法进行置换
顺便说说keep-alive在各个生命周期里都做了啥吧:
created:初始化一个cache、keys,前者用来存缓存组件的虚拟dom集合,后者用来存缓存组件的key集合mounted:实时监听include、exclude这两个的变化,并执行相应操作destroyed:删除掉所有缓存相关的东西
之前说了,
keep-alive不会被渲染到页面上,所以abstract这个属性至关重要!
// src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true, // 判断此组件是否需要在渲染成真实DOM
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created() {
this.cache = Object.create(null) // 创建对象来存储 缓存虚拟dom
this.keys = [] // 创建数组来存储 缓存key
},
mounted() {
// 实时监听include、exclude的变动
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
destroyed() {
for (const key in this.cache) { // 删除所有的缓存
pruneCacheEntry(this.cache, key, this.keys)
}
},
render() {
// 下面讲
}
}
pruneCacheEntry函数
咱们上面实现的生命周期destroyed中,执行了删除所有缓存这个操作,而这个操作是通过调用pruneCacheEntry来实现的,那咱们来说说pruneCacheEntry里做了啥吧
// src/core/components/keep-alive.js
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() // 执行组件的destory钩子函数
}
cache[key] = null // 设为null
remove(keys, key) // 删除对应的元素
}
总结一下就是做了三件事:
- 1、遍历集合,执行所有缓存组件的
$destroy方法 - 2、将
cache对应key的内容设置为null - 3、删除
keys中对应的元素
render函数
以下称
include为白名单,exclude为黑名单render函数里主要做了这些事:
- 第一步:获取到
keep-alive包裹的第一个组件以及它的组件名称 - 第二步:判断此
组件名称是否能被白名单、黑名单匹配,如果不能被白名单匹配 || 能被黑名单匹配,则直接返回VNode,不往下执行,如果不符合,则往下执行第三步 - 第三步:根据
组件ID、tag生成缓存key,并在缓存集合中查找是否已缓存过此组件。如果已缓存过,直接取出缓存组件,并更新缓存key在keys中的位置(这是LRU算法的关键),如果没缓存过,则继续第四步 - 第四步:分别在
cache、keys中保存此组件以及他的缓存key,并检查数量是否超过max,超过则根据LRU算法进行删除 - 第五步:将此组件实例的
keepAlive属性设置为true,这很重要哦,下面会讲到的!
// src/core/components/keep-alive.js
render() {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot) // 找到第一个子组件对象
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) { // 存在组件参数
// 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
}
const { cache, keys } = this
const key: ?string = vnode.key == null // 定义组件的缓存key
// 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) // 调整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)
}
}
vnode.data.keepAlive = true // 渲染和执行被包裹组件的钩子函数需要用到
}
return vnode || (slot && slot[0])
}
渲染
咱们先来看看Vue一个组件是怎么渲染的,咱们从render开始说:
render:此函数会将组件转成VNodepatch:此函数在初次渲染时会直接渲染根据拿到的VNode直接渲染成真实DOM,第二次渲染开始就会拿VNode会跟旧VNode对比,打补丁(diff算法对比发生在此阶段),然后渲染成真实DOM

keep-alive本身渲染
刚刚说了,keep-alive自身组件不会被渲染到页面上,那是怎么做到的呢?其实就是通过判断组件实例上的abstract的属性值,如果是true的话,就跳过该实例,该实例也不会出现在父级链上
// src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
const options = vm.$options
// 找到第一个非abstract的父组件实例
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
// ...
}
包裹组件渲染
咱们再来说说被keep-alive包裹着的组件是如何使用缓存的吧。刚刚说了VNode -> 真实DOM是发生在patch的阶段,而其实这也是要细分的:VNode -> 实例化 -> _update -> 真实DOM,而组件使用缓存的判断就发生在实例化这个阶段,而这个阶段调用的是createComponent函数,那我们就来说说这个函数吧:
// src/core/vdom/patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm) // 将缓存的DOM(vnode.elm)插入父元素中
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
- 在第一次加载被包裹组件时,因为
keep-alive的render先于包裹组件加载之前执行,所以此时vnode.componentInstance的值是undefined,而keepAlive是true,则代码走到i(vnode, false /* hydrating */)就不往下走了 - 再次访问包裹组件时,
vnode.componentInstance的值就是已经缓存的组件实例,那么会执行insert(parentElm, vnode.elm, refElm)逻辑,这样就直接把上一次的DOM插入到了父元素中。
结语
我是林三心,一个热心的前端菜鸟程序员。如果你上进,喜欢前端,想学习前端,那咱们可以交朋友,一起摸鱼哈哈,摸鱼群,点这个 --> 摸鱼沸点

参考
转载自:https://juejin.cn/post/7043401297302650917