我的回答

组件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的实现

  1. const patternTypes:Array<Function>=[String,RegExp,Array]// 接收:字符串、正则、数组
  2. export default {
  3. name: 'keep-alive',
  4. abstract: true,//一个抽象组件,自身不会渲染一个DOM元素,也不会出现在父组件中
  5. props:{
  6. include: patternTypes,//匹配的组件,缓存
  7. exclude: patternTypes,//不去匹配的组件,你缓存
  8. max: [String,Number],//缓存组件的最大实例数量,由于缓存的是组件实例(vnode),数量过多的时候,会占用过多的内存,可以用max指定上限
  9. },
  10. create(){
  11. //用于初始化缓存虚拟DOM数组和vnode的key
  12. this.cache=Object.create(null)
  13. this.keys=[]
  14. },
  15. destroyed(){
  16. //销毁缓存cache的组件实例
  17. for(const key in this.cache){
  18. pruneCacheEntry(this.cache,key,this.keys)
  19. }
  20. },
  21. mounted(){
  22. //监控include和exclude的改变,根据最新的include和exclude的内容,来实时削减缓存的组建的内容
  23. this.$watch('include',(val)=>{
  24. pruneCache(this,(name=>matches(val,name)))
  25. })
  26. this.$watch('exclude',(val)=>{
  27. pruneCache(this,(name)=>!matches(val,name))
  28. })
  29. },
  30. }

render函数

  1. 会在keep-alive组件内部去写自己的内容,所以可以去获取默认slot的内容,然后根据这个去获取组件
  2. keep-alive只对第一个组件有效,所以获取第一个子组件
  3. 和keep-alive搭配使用的一般有动态组件route-view
    1. render(){
    2. function getFirstComponentChild(children:?Array<VNode>):?VNode{
    3. if(Array.isArray(children)){
    4. for(var i=0;i< children.length;i++){
    5. const c=children[i]
    6. if(isDef(c)&&isDef(c.componentOptions) || isAsyncPlaceholder(c)){
    7. return c
    8. }
    9. }
    10. }
    11. const slot = this.$slots.default//获取默认插槽
    12. const vnode:VNode = getFirstComponentChild(slot)//获取第一个子组件
    13. const componentOptions:?VNodeComponentOptions = vnode && vnode.componentOptions//组件参数
    14. if(componentOptions){//是否有组件参数
    15. const name:?string = getComponentName(componentOptions)//获得组件名字
    16. const {include,exclude}=this
    17. if(
    18. //not include
    19. (include && (!name || !matches(include,name))) ||
    20. //excluded
    21. (exclude && name && matches(exclude,name))
    22. ){
    23. //如果不匹配当前组件的名字和include以及exclude
    24. //那么直接返回组件的实例
    25. return vnode
    26. }
    27. const {cache,keys}=this
    28. //获取这个组件的key
    29. const key:?string=vnode.key == null?componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}`:''):vnode.key
    30. if(cache[key]){
    31. //LRU缓存策略执行
    32. vnode.componentInstance=cache[key].componentInstance//组件初次渲染的时候componentInstance为undefined
    33. remove(keys,key)
    34. keys.push(key)
    35. //根据LRU缓存策略进行,将key从原来的位置移除,然后将这个key值放到最后
    36. }else{
    37. //在缓存列表里没有的话,则加入,同时判断当前加入之后,是否超过了max所设定的范围,是的话那么就移除
    38. //使用时间间隔最长的一个
    39. cache[key]=vnode
    40. keys.push(key)
    41. if(this.max && keys.length >parseInt(this.max)){
    42. pruneCacheEntry(cache,key[0],keys,this._vnode)
    43. }
    44. }
    45. //将组件的keepAlive属性设置为true
    46. vnode.data.keepAlive=true//判断是否执行组件的created、mounted生命周期函数
    47. }
    48. }
    49. return vnode||(slot && slot[0])
    50. }

keep-alive 具体是通过cache数组缓存所有组件的vnode实例。

当cache内原有组件被使用时会将该组件key从keys数组中删除,然后push到keys数组最后面,方便清除最不常用组件
步骤总结

  1. 获取keep-alive下第一个子组件的实例对象,通过它去获取这个组件的名字
  2. 通过当前组件名去匹配原来include和exclude,判断当前组件是否需要缓存,不需要缓存直接返回当前组件的实例vnode
  3. 需要缓存,判断它当前是否在缓存数组里面,存在的话就将它原来的位置上的key给移除,同时将这个组件的key放到数组最后面
  4. 不存在的话,将组件key放入数组,然后判断当前key数组是否超过max所设置的范围,超过的话那就削减没使用时间最长的一个组件的key值
  5. 最后将这个组件的keepAlive设置为true

    keep-alive本身的创建过程和patch过程

缓存渲染的时候,会根据vnode.componentInstance(首次渲染 vnode.componentInstance为undefined)和keepAlive属性判断不会执行组件的created、mounted等钩子函数,而是对缓存的组件执行patch过程:直接把缓存的DOM对象直接插入到目标元素中,完成了数据更新情况下的渲染过程

  • 首次渲染 组件的首次渲染:判断组件的abstract属性,才往父组件里面挂载DOM
    1. function initLifecycle(vm:Component){
    2. const options=vm.$options
    3. let parent=options.parent
    4. if(parent && !options.abstract){//判断组件的abstract属性,才往父组件里面挂载DOM
    5. while(parent.$options.abstract && parent.$parent){
    6. parent=parent.$parent
    7. }
    8. parent.$children.push(vm)
    9. }
    10. vm.$parent=parent
    11. vm.$root=parent?parent:$root:vm
    12. vm.$children=[]
    13. vm.$refs={}
    14. vm._watcher=null
    15. vm._inactive=null
    16. vm._directInactive=false
    17. vm._isMounted=false
    18. vm._isDestroyed=false
    19. vm._isBeingDestoryed=false
    20. }

判断当前keepAlive和componentInstance是否存在来判断是否要执行组件perpatch还是执行创建componentInstance

  1. init(vnode:VNodeWithData,hydrating:boolean):?boolean{
  2. if(vnode.componentInstance && !vnode.componentInstance.__isDestroyed && vnode.data.keepAlive){//首次渲染 vnode.componentInstance为undefined
  3. const mounteNode:any=vnode
  4. componentVNodeHooks.prepatch(mountedNode,mountedNode)//prepatch函数执行的是组件更新的过程
  5. }else{
  6. const child=vnode.componentInstance=createComponentInstanceForVnode(vnode,activeInstance)
  7. }
  8. child.$mount(hydrating?vode.elm:undefined,hydrating)
  9. }

prepatch操作就不会在执行组件的mounted和created声明周期函数,而是直接将DOM插入

LRU(least recently used)缓存策略

LRU缓存策略:从内存找出最久未使用的数据并置换新的数据 LRU(least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是:如果数据最近被访问过,那么将来被访问的几率也更高。最常见的实现是使用一个链表保存缓存数据,详细算法实现如下

  1. 新数据插入到链表头部
  2. 每当缓存数据被访问,则将数据移到链表头部
  3. 链表满的时候,将链表尾部的数据丢弃