重学Vue组件<keep-alive>
一、学习目标
- keep-alive是怎样实现缓存的
- 简单了解 activeed , deactived 钩子函数
二、概念
直译:“存活”、 “缓存” 、“保持状态” 、“持久化”
官方:使用 keep-alive 包裹动态组件时,会 缓存 不活动的组件实例,而不是销毁它们
<!-- 基本 -->
<keep-alive include="test">
<test></test>
<demo></demo>
<comp1></comp1>
</keep-alive>
属性:
- include - 字符串或正则表达式或数组。只有名称匹配的组件会被缓存。
- exclude - 字符串或正则表达式或数组。任何名称匹配的组件都不会被缓存。
- max - 数字。最多可以缓存多少组件实例。
实例:
<!-- 逗号分隔字符串 -->
<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>
<!-- exclude a组件不会被缓存 -->
<keep-alive exclude="a">
<component :is="view"></component>
</keep-alive>
<!-- 最多可以缓存多少组件实例。一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉 -->
<keep-alive :max="10">
<component :is="view"></component>
</keep-alive>
三、项目中使用场景之一
目的:主要用于 保留组件状态 或 避免重新渲染
<div class="app-content-wrapper comp-common-apply-detail">
<el-tabs v-model="componentName" v-tabs-permission="tabList">
<el-tab-pane
v-for="item in tabsListRequired"
:key="item.permissionId"
:label="item.tabName"
:name="item.compName"
/>
</el-tabs>
<div class="app-content-body">
<transition name="fade" mode="out-in">
<keep-alive include="CompBaseInfo,CompApproveFlowDetail,CompAttachments,CompFinancingPlanInfo">
<component :is="componentName"/>
</keep-alive>
</transition>
</div>
</div>
四、实现原理
官方:一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中
源码 ( https://github.com/vuejs/vue/blob/dev/src/core/components/keep-alive.js ) 【Show Me Code】
- 1、证明是一个组件
- 2、抽象组件?
- 3、为什么我们能传 include、exclude、max,以及这些属性不同数据类型的兼容性处理?
- 4、如何缓存?
- 5、缓存的是什么?
内置组件
const patternTypes: Array<Function> = [String, RegExp, Array]
export default {
name: 'keep-alive',
abstract: true,//标识是一个抽象组件。它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中(感兴趣的可以深挖)
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
...
methods:{},
}
//匹配组件名称
//@param pattern 匹配对象(include、exlude)
//@param name 组件名称
function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
if (Array.isArray(pattern)) { //数组
return pattern.indexOf(name) > -1
} else if (typeof pattern === 'string') { //字符串
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) { //正则
return pattern.test(name)
}
/* istanbul ignore next */
return false
}
建一个“存储池”
created () {
this.cache = Object.create(null) //创建缓存对象 this.cache = { 'key1':'组件1', 'key2':'组件2' }
this.keys = [] //创建保存组件key的数组,key就是this.cache中的键值
}
如何进行缓存 -render()
- 1.获取第一个子组件的节点
const slot = this.$solts.default
const vnode = getFirstComponentChild(solt)
// 只处理第一个子元素,所以一般和它搭配使用的有 component 动态组件或者是 router-view
- 2.获取该组件的名称
/* 获取该组件节点的名称 */
const name = getComponentName(componentOptions)
/* 优先获取组件的name字段,如果name不存在则获取组件的tag */
function getComponentName (opts: ?VNodeComponentOptions): ?string {
return opts && (opts.Ctor.options.name || opts.tag)
}
- 3.拿到的组件名称和include、exclude中的值做match
// 如果该组件不做缓存-则返回该组件 (vnode)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
// 否则-缓存下来
const { cache, keys } = this
// 获取组件的key值
const key = vnode.key == null?
componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
//拿到key值后取this.cach对象中寻找是否有该值,如果有,说明组件有缓存,命中缓存
// 如果命中缓存,则直接从缓存中拿 vnode 的组件实例 //
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
/* 调整该组件key的顺序,将其从原来的地方删掉并重新放在最后一个 */
// LRU 缓存机制(Least recently used,最近最少使用)
remove(keys, key) //删除你
keys.push(key) //再加你-把你放在心底
} else {
// 如果没有,则将组件存入缓存
this.vnodeToCache = vnode
this.keyToCache = key
// cacheVNode()
}
cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this
if (vnodeToCache) { //被缓存的组件vnode
const { tag, componentInstance, componentOptions } = vnodeToCache
cache[keyToCache] = {
name: getComponentName(componentOptions),
tag,
componentInstance,
}
keys.push(keyToCache)
// 移除最旧的vnode
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
}
生命周期 (src/core/instance/lifecycle.js)
- 我们都知道组件一旦被keep-alive缓存,那么再次渲染的时候就不会执行 created、mounted 等钩子函数
- 当组件在 keep-alive 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行
- 它的执行时机是 keep-alive 包裹的组件渲染的时候
- 在渲染的最后一步,会执行 invokeInsertHook() 函数执行 vnode 的 insert() 钩子函数,insert() 函数主要逻辑是执行 activateChildComponent() 函数.
- 执行组件的 acitvated 钩子函数,并且递归去执行它的所有子组件的 activated 钩子函数
export function activateChildComponent (vm: Component, direct?: boolean) {
...
if (vm._inactive || vm._inactive === null) {
vm._inactive = false
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i])
}
callHook(vm, 'activated')
}
}
- 同样在组件销毁时,执行 destroy()函数中的 deactivateChildComponent()
- 执行组件的 deacitvated 钩子函数,并且递归去执行它的所有子组件的 deactivated 钩子函数
export function deactivateChildComponent (vm: Component, direct?: boolean) {
...
if (!vm._inactive) {
vm._inactive = true
for (let i = 0; i < vm.$children.length; i++) {
deactivateChildComponent(vm.$children[i])
}
callHook(vm, 'deactivated')
}
}
五、总结
- keep-alive 是vue的一个内置组件,主要是用来控制组件的缓存,它可以设置三个参数,来设置哪个组件需要被缓存,或者不被缓存,同时还有缓存阙值
- 缓存的是组件的vnode,都不需要再次创建Vue实例,而是用之前缓存的实例
- keep-alive的缓存策略使用的是LRU缓存淘汰策略
六、扩展
- 1、抽象组件为什么不出现在组件的父组件链中
// initLifecycle
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
- 2、LRU 缓存机制-(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”
- 3、虚拟DOM (VNode):在Vue中,VNode对象来描述真实dom节点,对象包括标签名、数据、子节点、键值等一些属性
- 4、keep-alive组件渲染过程,初次渲染和缓存渲染有什么不同?