通俗易懂了解Vue内置组件keep-alive内部原理
1. 官⽅介绍及其⽤法
1.1 组件介绍
要想搞明⽩<keep-alive>组件的内部实现原理,⾸先我们得搞明⽩这个组件怎么⽤以及为什么要⽤它,关于<keep-alive>组件,官⽹如下介绍:<keep-alive>是Vue中内置的⼀个抽象组件,它⾃⾝不会渲染⼀个DOM元素,也不会出现在⽗组件链中。当它包裹动态组件时,会缓存不活动的组件实例,⽽不是销毁它们。
这句话的意思简单来说:就是我们可以把⼀些不常变动的组件或者需要缓存的组件⽤<keep-alive>包裹起来,这样<keep-alive>就会帮我们把组件保存在内存中,⽽不是直接的销毁,这样做可以保留组件的状态或避免多次重新渲染,以提⾼页⾯性能。
1.2 ⽤法
<keep-alive>组件可接收三个属性:
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选项,如果name选项不可⽤,则匹配它的局部注册名称 (⽗组件components选项的键值),也就是组件的标签值。匿名组件不能被匹配。
max表⽰最多可以缓存多少组件实例。⼀旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉。
请读者注意此处加粗的地⽅,暂时有个印象,后⾯我们会详细说明。
<keep-alive :max="10">
<component :is="view"></component>
</keep-alive>
OK,以上就是<keep-alive>组件的官⽅介绍及其⽤法,下⾯我们将着重介绍其内部实现原理。
2. 实现原理
<keep-alive>是Vue源码中实现的⼀个组件,也就是说Vue源码不仅实现了⼀套组件化的机制,也实现了⼀些内置组件,该组件的定义在
src/core/components/keep-alive.js中:
export default {
name: 'keep-alive',
abstract: true,
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
},
created () {
this.cache = ate(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.default
const vnode = getFirstComponentChild(slot)
/* 获取该组件节点的componentOptions */
const componentOptions = vnode && vnodeponentOptions
if (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 } = 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]) {
vnodeponentInstance = cache[key]ponentInstance
// make current key freshest
remove(keys, key)
keys.push(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])
}
}
这是该组件的源码,是不是突然看到⼀⼤⽚代码有点惊慌失措,哈哈哈,不要急不要慌,接下来我们抽丝剥茧,逐个击破。
⾸先我们可以看到该组件内没有常规的<template></template>等标签,因为它不是⼀个常规的模板组件,取⽽代之的是它内部多了⼀个叫做render的函数,它是⼀个函数式组件,执⾏<keep-alive>组件渲染的时候,就会执⾏到这个render函数。了解了这个以后,接下来我们从上到下⼀步⼀步细细阅读。
2.1 props
在props选项内接收传进来的三个属性:include、exclude和max。如下:
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
}
include表⽰只有匹配到的组件会被缓存,⽽exclude表⽰任何匹配到的组件都不会被缓存,max表⽰缓存组件的数量,因为我们是缓存的vnode对象,它也会持有 DOM,当我们缓存的组件很多的时候,会⽐较占⽤内存,所以该配置允许我们指定缓存组件的数量。
2.2 created
在created钩⼦函数⾥定义并初始化了两个属性:this.cache和this.keys。
created () {
this.cache = ate(null)
this.keys = []
}
this.cache是⼀个对象,⽤来存储需要缓存的组件,它将以如下形式存储:
this.cache = {
'key1':'组件1',
'key2':'组件2',
// ...
}
this.keys是⼀个数组,⽤来存储每个需要缓存的组件的key,即对应this.cache对象中的键值。
2.3 destroyed
当<keep-alive>组件被销毁时,此时会调⽤destroyed钩⼦函数,在该钩⼦函数⾥会遍历this.cache对象,然后将那些被缓存的并且当前没有处于被渲染状态的组件都销毁掉。如下:
destroyed () {正则匹配原理
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
}
// pruneCacheEntry函数
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
/* 判断当前没有处于被渲染状态的组件,将其销毁*/
if (cached && (!current || cached.tag !== current.tag)) {
cachedponentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
2.4 mounted
在mounted钩⼦函数中观测include和exclude的变化,如下:
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
}
如果include或exclude发⽣了变化,即表⽰定义需要缓存的组件的规则或者不需要缓存的组件的规则发⽣了变化,那么就执⾏pruneCache函数,函数如下:
function pruneCache (keepAliveInstance, filter) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode = cache[key]
if (cachedNode) {
const name = getComponentName(cachedNodeponentOptions)
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
在该函数内对this.cache对象进⾏遍历,取出每⼀项的name值,⽤其与新的缓存规则进⾏匹配,如果匹配不上,则表⽰在新的缓存规则下该组件已经不需要被缓存,则调⽤pruneCacheEntry函数将其从this.cache对象剔除即可。
2.5 render
接下来就是重头戏render函数,也是本篇⽂章中的重中之重。以上⼯作都是⼀些辅助⼯作,真正实现缓存功能的就在这个render函数⾥,接下来我们逐⾏分析它。
在render函数中⾸先获取第⼀个⼦组件节点的vnode:
/* 获取默认插槽中的第⼀个组件节点 */
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)
由于我们也是在<keep-alive>标签内部写 DOM,所以可以先获取到它的默认插槽,然后再获取到它的第⼀个⼦节点。<keep-alive>只处理第⼀个⼦元素,所以⼀般和它搭配使⽤的有component动态组件或者是router-view。
接下来获取该组件节点的名称:
/* 获取该组件节点的名称 */
const name = getComponentName(componentOptions)
/* 优先获取组件的name字段,如果name不存在则获取组件的tag */
function getComponentName (opts: ?VNodeComponentOptions): ?string {
return opts && (opts.Ctor.options.name || opts.tag)
}
然后⽤组件名称跟include、exclude中的匹配规则去匹配:
const { include, exclude } = this
/* 如果name与include规则不匹配或者与exclude规则匹配则表⽰不缓存,直接返回vnode */
if (
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
如果组件名称与include规则不匹配或者与exclude规则匹配,则表⽰不缓存该组件,直接返回这个组件的vnode,否则的话⾛下⼀步缓存:
const { cache, keys } = this
/* 获取组件的key */
const key = vnode.key == null
componentOptions.Ctor.cid + (componentOptions.tag `::${componentOptions.tag}` : '')
: vnode.key
/* 如果命中缓存,则直接从缓存中拿 vnode 的组件实例 */
if (cache[key]) {
vnodeponentInstance = cache[key]ponentInstance
/* 调整该组件key的顺序,将其从原来的地⽅删掉并重新放在最后⼀个 */
remove(keys, key)
keys.push(key)
}
/* 如果没有命中缓存,则将其设置进缓存 */
else {
cache[key] = vnode
keys.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
⾸先获取组件的key值:
const key = vnode.key == null?
componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
拿到key值后去this.cache对象中去寻是否有该值,如果有则表⽰该组件有缓存,即命中缓存:
/* 如果命中缓存,则直接从缓存中拿 vnode 的组件实例 */
if (cache[key]) {
vnodeponentInstance = cache[key]ponentInstance
/* 调整该组件key的顺序,将其从原来的地⽅删掉并重新放在最后⼀个 */
remove(keys, key)
keys.push(key)
}
直接从缓存中拿vnode的组件实例,此时重新调整该组件key的顺序,将其从原来的地⽅删掉并重新放在this.keys中最后⼀个。
如果this.cache对象中没有该key值:
/* 如果没有命中缓存,则将其设置进缓存 */
else {
cache[key] = vnode
keys.push(key)
/* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第⼀个 */
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
表明该组件还没有被缓存过,则以该组件的key为键,组件vnode为值,将其存⼊this.cache中,并且把key存⼊this.keys中。此时再判断this.keys中缓存组件的数量是否超过了设置的最⼤缓存数量值this.max,如果超过了,则把第⼀个缓存组件删掉。
那么问题来了:为什么要删除第⼀个缓存组件并且为什么命中缓存了还要调整组件key的顺序?
这其实应⽤了⼀个缓存淘汰策略LRU:
LRU(Least recently used,最近最少使⽤)算法根据数据的历史访问记录来进⾏淘汰数据,其核⼼思想是“如果数据最近被访问过,那么
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论