我的回答
组件options的存储基于LRU缓存
在编译阶段识别是否被keep-alive劫持, 如果被劫持, 组件的options数据被放到缓存中, 下次渲染匹配的时候使用缓存的options的数据
参考回答
keep-alive
props
- include字符串或正则表达式,只有名称匹配的组件会被匹配
- exclude字符串或正则表达式。任何名称匹配的组件都不会被缓存
- max数字。最多可以缓存多少组件实例
- keep-alive 包裹动态组件时,会缓存不活动的组件实例
主要流程
- 判断组件
name
,不在include
或者在exclude
中,直接返回vnode
,说明该组件不被缓存 - 获取组件实例key,如果有获取实例的
key
,否则重新生成。 - key生成规则,
cid+"::"+tag
,仅靠cid是不够的,因为相同的构造函数可以注册为不同的本地组件 - 如果缓存对象内存在,则直接从缓存对象中获取组件实例给vnode,不存在则添加到缓存对象中
- 最大缓存数量,当缓存数量超过max值时,清除keys数组内的第一个组件
keep-alive的实现
const patternTypes:Array<Function>=[String,RegExp,Array]// 接收:字符串、正则、数组
export default {
name: 'keep-alive',
abstract: true,//一个抽象组件,自身不会渲染一个DOM元素,也不会出现在父组件中
props:{
include: patternTypes,//匹配的组件,缓存
exclude: patternTypes,//不去匹配的组件,你缓存
max: [String,Number],//缓存组件的最大实例数量,由于缓存的是组件实例(vnode),数量过多的时候,会占用过多的内存,可以用max指定上限
},
create(){
//用于初始化缓存虚拟DOM数组和vnode的key
this.cache=Object.create(null)
this.keys=[]
},
destroyed(){
//销毁缓存cache的组件实例
for(const key in this.cache){
pruneCacheEntry(this.cache,key,this.keys)
}
},
mounted(){
//监控include和exclude的改变,根据最新的include和exclude的内容,来实时削减缓存的组建的内容
this.$watch('include',(val)=>{
pruneCache(this,(name=>matches(val,name)))
})
this.$watch('exclude',(val)=>{
pruneCache(this,(name)=>!matches(val,name))
})
},
}
render函数
- 会在keep-alive组件内部去写自己的内容,所以可以去获取默认slot的内容,然后根据这个去获取组件
- keep-alive只对第一个组件有效,所以获取第一个子组件
- 和keep-alive搭配使用的一般有
动态组件
和route-view
render(){
function getFirstComponentChild(children:?Array<VNode>):?VNode{
if(Array.isArray(children)){
for(var i=0;i< children.length;i++){
const c=children[i]
if(isDef(c)&&isDef(c.componentOptions) || isAsyncPlaceholder(c)){
return c
}
}
}
const slot = this.$slots.default//获取默认插槽
const vnode:VNode = getFirstComponentChild(slot)//获取第一个子组件
const componentOptions:?VNodeComponentOptions = vnode && vnode.componentOptions//组件参数
if(componentOptions){//是否有组件参数
const name:?string = getComponentName(componentOptions)//获得组件名字
const {include,exclude}=this
if(
//not include
(include && (!name || !matches(include,name))) ||
//excluded
(exclude && name && matches(exclude,name))
){
//如果不匹配当前组件的名字和include以及exclude
//那么直接返回组件的实例
return vnode
}
const {cache,keys}=this
//获取这个组件的key
const key:?string=vnode.key == null?componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}`:''):vnode.key
if(cache[key]){
//LRU缓存策略执行
vnode.componentInstance=cache[key].componentInstance//组件初次渲染的时候componentInstance为undefined
remove(keys,key)
keys.push(key)
//根据LRU缓存策略进行,将key从原来的位置移除,然后将这个key值放到最后
}else{
//在缓存列表里没有的话,则加入,同时判断当前加入之后,是否超过了max所设定的范围,是的话那么就移除
//使用时间间隔最长的一个
cache[key]=vnode
keys.push(key)
if(this.max && keys.length >parseInt(this.max)){
pruneCacheEntry(cache,key[0],keys,this._vnode)
}
}
//将组件的keepAlive属性设置为true
vnode.data.keepAlive=true//判断是否执行组件的created、mounted生命周期函数
}
}
return vnode||(slot && slot[0])
}
keep-alive 具体是通过cache数组缓存所有组件的vnode实例。
当cache内原有组件被使用时会将该组件key从keys数组中删除,然后push到keys数组最后面,方便清除最不常用组件
步骤总结
- 获取keep-alive下第一个子组件的实例对象,通过它去获取这个组件的名字
- 通过当前组件名去匹配原来include和exclude,判断当前组件是否需要缓存,不需要缓存直接返回当前组件的实例vnode
- 需要缓存,判断它当前是否在缓存数组里面,存在的话就将它原来的位置上的key给移除,同时将这个组件的key放到数组最后面
- 不存在的话,将组件key放入数组,然后判断当前key数组是否超过max所设置的范围,超过的话那就削减没使用时间最长的一个组件的key值
- 最后将这个组件的keepAlive设置为true
keep-alive本身的创建过程和patch过程
缓存渲染的时候,会根据vnode.componentInstance(首次渲染 vnode.componentInstance为undefined)和keepAlive属性判断不会执行组件的created、mounted等钩子函数,而是对缓存的组件执行patch过程:直接把缓存的DOM对象直接插入到目标元素中,完成了数据更新情况下的渲染过程
- 首次渲染 组件的首次渲染:判断组件的abstract属性,才往父组件里面挂载DOM
function initLifecycle(vm:Component){
const options=vm.$options
let parent=options.parent
if(parent && !options.abstract){//判断组件的abstract属性,才往父组件里面挂载DOM
while(parent.$options.abstract && parent.$parent){
parent=parent.$parent
}
parent.$children.push(vm)
}
vm.$parent=parent
vm.$root=parent?parent:$root:vm
vm.$children=[]
vm.$refs={}
vm._watcher=null
vm._inactive=null
vm._directInactive=false
vm._isMounted=false
vm._isDestroyed=false
vm._isBeingDestoryed=false
}
判断当前keepAlive和componentInstance是否存在来判断是否要执行组件perpatch还是执行创建componentInstance
init(vnode:VNodeWithData,hydrating:boolean):?boolean{
if(vnode.componentInstance && !vnode.componentInstance.__isDestroyed && vnode.data.keepAlive){//首次渲染 vnode.componentInstance为undefined
const mounteNode:any=vnode
componentVNodeHooks.prepatch(mountedNode,mountedNode)//prepatch函数执行的是组件更新的过程
}else{
const child=vnode.componentInstance=createComponentInstanceForVnode(vnode,activeInstance)
}
child.$mount(hydrating?vode.elm:undefined,hydrating)
}
prepatch操作就不会在执行组件的mounted和created声明周期函数,而是直接将DOM插入
LRU(least recently used)缓存策略
LRU缓存策略:
从内存找出最久未使用的数据并置换新的数据 LRU(least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是:如果数据最近被访问过,那么将来被访问的几率也更高。最常见的实现是使用一个链表保存缓存数据,详细算法实现如下
- 新数据插入到链表头部
- 每当缓存数据被访问,则将数据移到链表头部
- 链表满的时候,将链表尾部的数据丢弃