keep-alive官方文档
在动态组件上使用 keep-alive
prop:
include
- 字符串或正则表达式。只有名称匹配的组件会被缓存。exclude
- 字符串或正则表达式。任何名称匹配的组件都不会被缓存。max
- 数字。最多可以缓存多少组件实例。
是Vue的内置组件,能在组件切换过程
中将状态保留在内存
中,防止重复渲染DOM
。
抽象组件
:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。
当组件在
使用$route.meta
的keepAlive
属性:
<keep-alive>
<router-view v-if="$route.meta.keepAlive">router-view>
keep-alive>
<router-view v-if="!$route.meta.keepAlive">router-view>
需要在router
中设置router的元信息meta
:
//...router.js
export default new Router({
routes: [
...
{
path: '/page1',
name: 'Page1',
component: Page1,
meta: {
keepAlive: true // 缓存
}
},
{
path: '/page2',
name: 'Page2',
component: Page2,
meta: {
keepAlive: false // 不缓存
}
}
]
})
以上面router的代码为例:
<template>
<div class="page1">
<h1>Vueh1>
<h2>{
{msg}}h2>
<input placeholder="输入框">input>
div>
template>
<template>
<div class="page2">
<h1>{
{msg}}h1>
<input placeholder="输入框">input>
div>
template>
不同组件切换过程,有缓存的组件不会被销毁,也不用重新渲染
上面例子,可以看到,每次切换到page1组件时,可以看到input框依旧是上次填写的内容,不会重置,而page2组件的input框会重置
也可以通过路由的beforeRouteLeave(to, from, next)钩子
动态设置route.meta
的keepAlive
属性来实现其他需求
中的缓存优化遵循 LRU 原则
,所以了解下缓存淘汰策略。
由于缓存空间是有限的,所以不能无限制的进行数据存储,当存储容量达到一个阀值时,就会造成内存溢出
,因此在进行数据缓存时,就要根据情况对缓存进行优化,清除一些可能不会再用到的数据。
所以根据缓存淘汰的机制不同,常用的有以下三种:
通过记录数据使用的时间,当缓存大小即将溢出时,优先清除离当前时间最远的数据
。
以时间作为参考,如果数据最近被访问过,那么将来被访问的几率会更高,如果以一个数组去记录数据,当有一数据被访问时,该数据会被移动到数组的末尾,表明最近被使用过,当缓存溢出时,会删除数组的头部数据,即将最不频繁使用的数据移除
。(keep-alive 的优化处理)
以次数作为参考,用次数去标记数据使用频率,次数最少的会在缓存溢出时被淘汰
。
首先我们看一个动态组件使用 的例子
<div id="dynamic-component-demo">
<button v-on:click="currentTab = 'Posts'">Postsbutton>
<button v-on:click="currentTab = 'Archive'">Archivebutton>
<keep-alive>
<component
v-bind:is="currentTabComponent"
class="tab"
>component>
keep-alive>
div>
Vue.component('tab-posts', {
data: function () {
return {
count: 0
}
},
template: `
{
{count}}
`
})
Vue.component('tab-archive', {
template: 'Archive component'
})
new Vue({
el: '#dynamic-component-demo',
data: {
currentTab: 'Posts',
},
computed: {
currentTabComponent: function () {
return 'tab-' + this.currentTab.toLowerCase()
}
}
})
我们可以看到,动态组件外层包裹着
标签。
<keep-alive>
<component
v-bind:is="currentTabComponent"
class="tab"
>component>
keep-alive>
那就意味着,当选项卡 Posts 、 Archive 在来回切换时,所对应的组件实例会被缓存起来,所以当再次切换到 Posts 选项时,其对应的组件 tab-posts 会从缓存中获取,计数器 count 也会保留上一次的状态。
就此,我们看完
的简单示例之后,让我们一起来分析下源码中它是如何进行组件缓存和缓存优化处理的。
vue 在模板 -> AST -> render() -> vnode -> 真实Dom 这个转化过程中,会进入patch
阶段,在patch 阶段,会调用createElm 函数
中会将 vnode 转化为真实 dom
。
function createPatchFunction (backend) {
...
//生成真实dom
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// 返回 true 代表为 vnode 为组件 vnode,将停止接下来的转换过程
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return;
}
...
}
}
在转化节点的过程中,因为
会视为组件 vnode
,因此一开始会调用createComponent()
函数,createComponent()
会执行组件初始化内部钩子 init()
, 对组件进行初始化和实例化。
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
// isReactivated 用来判断组件是否缓存
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 执行组件初始化的内部钩子 init()
i(vnode, false /* hydrating */);
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
// 将真实 dom 添加到父节点,insert 操作 dom api
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
组件通过调用内部钩子 init() 方法进行初始化操作。
源码中通过函数 installComponentHooks()
可追踪到内部钩子的定义对象 componentVNodeHooks
。
// inline hooks to be invoked on component VNodes during patch
var componentVNodeHooks = {
init: function init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
} else {
// 第一次运行时,vnode.componentInstance 不存在 ,vnode.data.keepAlive 不存在
// 将组件实例化,并赋值给 vnode 的 componentInstance 属性
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
);
// 进行挂载
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
},
// prepatch 是 patch 过程的核心步骤
prepatch: function prepatch (oldVnode, vnode) {
... },
insert: function insert (vnode) {
... },
destroy: function destroy (vnode) {
... }
};
第一次执行时,很明显组件 vnode
没有 componentInstance
属性,vnode.data.keepAlive
也没有值,所以会调用 createComponentInstanceForVnode()
将组件进行实例化并将组件实例赋值给 vnode
的componentInstance
属性,最后执行组件实例的 $mount
方法进行实例挂载。
createComponentInstanceForVnode()
是组件实例化
的过程,组件实例化无非就是一系列选项合并,初始化事件,生命周期等初始化操作。
在执行组件实例化之后会进行组件的挂载(如上代码所示)。
...
// 进行挂载
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
...
挂载$mount
阶段会调用mountComponent()
函数进行 vm._update(vm._render(), hydrating)
操作。
Vue.prototype.$mount = function (el, hydrating) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
function mountComponent (vm, el, hydrating) {
vm.$el = el;
...
callHook(vm, 'beforeMount');
var updateComponent;
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
...
} else {
updateComponent = function () {
// vm._render() 会根据数据的变化为组件生成新的 Vnode 节点
// vm._update() 最终会为 Vnode 生成真实 DOM 节点
vm._update(vm._render(), hydrating);
}
}
...
return vm
}
而 vm._render()
函数最终会调用组件选项中的render() 函数
,进行渲染。
function renderMixin (Vue) {
...
Vue.prototype._render = function () {
var vm = this;
var ref = vm.$options;
var render = ref.render;
...
try {
...
// 调用组件的 render 函数
vnode = render.call(vm._renderProxy, vm.$createElement);
}
...
return vnode
};
}
由于keep-alive 是一个内置组件,因此也拥有自己的 render() 函数,所以让我们一起来看下 render() 函数的具体实现。
var KeepAlive = {
...
props: {
include: patternTypes, // 名称匹配的组件会被缓存,对外暴露 include 属性 api
exclude: patternTypes, // 名称匹配的组件不会被缓存,对外暴露 exclude 属性 api
max: [String, Number] // 可以缓存的组件最大个数,对外暴露 max 属性 api
},
created: function created () {
},
destroyed: function destroyed () {
},
mounted: function mounted () {
},
// 在渲染阶段,进行缓存的存或者取
render: function render () {
// 首先拿到 keep-alve 下插槽的默认值 (包裹的组件)
var slot = this.$slots.default;
// 获取第一个 vnode 节点
var vnode = getFirstComponentChild(slot); // # 3802 line
// 拿到第一个子组件实例
var componentOptions = vnode && vnode.componentOptions;
// 如果 keep-alive 第一个组件实例不存在
if (componentOptions) {
// check pattern
var name = getComponentName(componentOptions);
var ref = this;
var include = ref.include;
var exclude = ref.exclude;
// 根据匹配规则返回 vnode
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
var ref$1 = this;
var cache = ref$1.cache;
var keys = ref$1.keys;
var key = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
// 获取本地组件唯一key
? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
: vnode.key;
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
// 使用 LRU 最近最少缓存策略,将命中的 key 从缓存数组中删除,并将当前最新 key 存入缓存数组的末尾
remove(keys, key); // 删除命中已存在的组件
keys.push(key); // 将当前组件名重新存入数组最末端
} else {
// 进行缓存
cache[key] = vnode;
keys.push(key);
// prune oldest entry
// 根据组件名与 max 进行比较
if (this.max && keys.length > parseInt(this.max)) {
// 超出组件缓存最大数的限制
// 执行 pruneCacheEntry 对最少访问数据(数组的第一项)进行删除
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
// 为缓存组件打上标志
vnode.data.keepAlive = true;
}
// 返回 vnode
return vnode || (slot && slot[0])
}
};
从上可得知,在 keep-alive
的源码定义中, render()
阶段会缓存 vnode
和组件名称 key
等操作。
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
// 使用 LRU 最近最少缓存策略,将命中的 key 从缓存数组中删除,并将当前最新 key 存入缓存数组的末尾
remove(keys, key); // 删除命中已存在的组件
keys.push(key); // 将当前组件名重新存入数组最末端
} else {
// 进行缓存
cache[key] = vnode;
keys.push(key);
// prune oldest entry
// 根据组件名与 max 进行比较
if (this.max && keys.length > parseInt(this.max)) {
// 超出组件缓存最大数的限制
// 执行 pruneCacheEntry 对最少访问数据(数组的第一项)进行删除
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
回顾之前提到的首次渲染
阶段,会调用 createComponent()
函数, createComponent()
会执行组件初始化内部钩子 init()
,对组件进行初始化和实例化等操作。
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
// isReactivated 用来判断组件是否缓存
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 执行组件初始化的内部钩子 init()
i(vnode, false /* hydrating */);
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
// 将真实 dom 添加到父节点,insert 操作 dom api
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
createComponet()
函数还会我们通过 vnode.componentInstance
拿到了
组件的实例,然后执行initComponent()
,
initComponent()
函数的作用就是将真实的 dom 保存在 vnode 中。
...
if (isDef(vnode.componentInstance)) {
// 其中的一个作用就是保存真实 dom 到 vnode 中
initComponent(vnode, insertedVnodeQueue);
// 将真实 dom 添加到父节点,(insert 操作 dom api)
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
...
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
vnode.data.pendingInsert = null;
}
// 保存真是 dom 节点到 vnode
vnode.elm = vnode.componentInstance.$el;
...
}
vue 中对 的缓存优化处理的实现上,便用到了上述的 LRU
缓存策略 。
LRU 缓存策略 :
以时间作为参考,如果数据最近被访问过,那么将来被访问的几率会更高,如果以一个数组去记录数据,当有一数据被访问时,该数据会被移动到数组的末尾,表明最近被使用过,当缓存溢出时,会删除数组的头部数据,即将最不频繁使用的数据移除
上面介绍到,
组件在存取缓存的过程中,是在渲染阶段进行的,所以我们回过头来看 render() 函数的实现。
var KeepAlive = {
...
props: {
include: patternTypes, // 名称匹配的组件会被缓存,对外暴露 include 属性 api
exclude: patternTypes, // 名称匹配的组件不会被缓存,对外暴露 exclude 属性 api
max: [String, Number] // 可以缓存的组件最大个数,对外暴露 max 属性 api
},
// 创建节点生成缓存对象
created: function created () {
this.cache = Object.create(null); // 缓存 vnode
this.keys = []; // 缓存组件名
},
// 在渲染阶段,进行缓存的存或者取
render: function render () {
// 首先拿到 keep-alve 下插槽的默认值 (包裹的组件)
var slot = this.$slots.default;
// 获取第一个 vnode 节点
var vnode = getFirstComponentChild(slot); // # 3802 line
// 拿到第一个子组件实例
var componentOptions = vnode && vnode.componentOptions;
// 如果 keep-alive 第一个组件实例不存在
if (componentOptions) {
// check pattern
var name = getComponentName(componentOptions);
var ref = this;
var include = ref.include;
var exclude = ref.exclude;
// 根据匹配规则返回 vnode
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
var ref$1 = this;
var cache = ref$1.cache;
var keys = ref$1.keys;
var key = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
// 获取本地组件唯一key
? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
: vnode.key;
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
// 使用 LRU 最近最少缓存策略,将命中的 key 从缓存数组中删除,并将当前最新 key 存入缓存数组的末尾
remove(keys, key); // 删除命中已存在的组件
keys.push(key); // 将当前组件名重新存入数组最末端
} else {
// 进行缓存
cache[key] = vnode;
keys.push(key);
// prune oldest entry
// 根据组件名与 max 进行比较
if (this.max && keys.length > parseInt(this.max)) {
// 超出组件缓存最大数的限制
// 执行 pruneCacheEntry 对最少访问数据(数组的第一项)进行删除
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
// 为缓存组件打上标志
vnode.data.keepAlive = true;
}
// 返回 vnode
return vnode || (slot && slot[0])
}
};
if (cache[key]) {
...
// 使用 LRU 最近最少缓存策略,将命中的 key 从缓存数组中删除,并将当前最新 key 存入缓存数组的末尾
remove(keys, key); // 删除命中已存在的组件
keys.push(key); // 将当前组件名重新存入数组最末端
} else {
// 进行缓存
cache[key] = vnode;
keys.push(key);
// 根据组件名与 max 进行比较
if (this.max && keys.length > parseInt(this.max)) {
// 超出组件缓存最大数的限制
// 执行 pruneCacheEntry 对最少访问数据(数组的第一项)进行删除
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
从注释中我们可以得知,当 keep-alive 被激活时(触发 activated 钩子),会执行 remove(keys, key) 函数,从缓存数组中 keys 删除已存在的组件,之后会进行 push 操作,将当前组件名重新存入 keys 数组的最末端,正好符合 LRU 。
remove(keys, key); // 删除命中已存在的组件
keys.push(key); // 将当前组件名重新存入数组最末端
function remove (arr, item) {
if (arr.length) {
var index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1)
}
}
}
至此,我们可以回过头看我们上边的 示例,示例中包含 tab-posts、tab-archive 两个组件,通过 component 的 is 属性动态渲染。当 tab 来回切换时,会将两个组件的 vnode 和组件名称存入缓存中,如下。
keys = ['tab-posts', 'tab-archive']
cache = {
'tab-posts': tabPostsVnode,
'tab-archive': tabArchiveVnode
}
假如,当再次激活到 tabPosts 组件时,由于命中了缓存,会调用源码中的 remove()方法,从缓存数组中 keys 把 tab-posts 删除,之后会使用 push 方法将 tab-posts 推到末尾。这时缓存结果变为:
keys = ['tab-archive', 'tab-posts']
cache = {
'tab-posts': tabPostsVnode,
'tab-archive': tabArchiveVnode
}
现在我们可以得知,keys 用开缓存组件名是用来记录缓存数据的。 那么当缓存溢出时,
我们可以通过 max 属性来限制最多可以缓存多少组件实例。
在上面源码中的render()
阶段,还有一个 pruneCacheEntry(cache, keys[0], keys, this._vnode)
函数,根据 LRU 淘汰策略,会在缓存溢出时,删除缓存中的头部数据,所以会将 keys[0] 传入pruneCacheEntry()
。
if (this.max && keys.length > parseInt(this.max)) {
// 超出组件缓存最大数的限制
// 执行 pruneCacheEntry 对最少访问数据(数组的第一项)进行删除
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
pruneCacheEntry()
具体逻辑如下:
cached$$1 = cache[key]
获取头部数据对应的值 vnode
,执行 cached$$1.componentInstance.$destroy()
将组件实例销毁。上面就是关于 首次渲染
、组件缓存
和缓存优化处理
相关的实现
abstract
,通常会与动态组件一同使用。keep-alive实现原理
Vue源码解析,keep-alive是如何实现缓存的?
谢谢你阅读到了最后
期待你关注、收藏、评论、点赞