KeepAlive源码学习记录

此篇文章作为学习keepalive源码的一个记录,如有不对,请多多指教!!也有借鉴其他哥哥姐姐弟弟妹妹们的文章,非常感谢!!

简介

在性能优化上,我们最常见的手段就是缓存。对需要经常访问的资源进行缓存,减少请求或者是初始化的过程,从而降低时间或内存的消耗。

在Vue中就提供了这样一个内置组件- KeepAlive ,它是一个抽象组件,本身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。KeepAlive可用来缓存组件或者路由。把一些不常变动的组件或者需要缓存的组件用KeepAlive包裹起来,这样KeepAlive就会帮我们把组件保存在内存中,而不是直接的销毁,这样做可以保留组件的状态或避免多次重新渲染,以提高页面性能。

用法

KeepAlive可以接收三个属性:

  1. include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
  2. exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
  3. max - 最多可以缓存多少组件。

include 和 exclude 属性允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示

注意:

被KeepAlive 缓存的组件,第一次进入时会触发created、mounted等生命周期函数,当组件再次激活的时候就不会执行 created、mounted 等钩子函数,但是很多业务场景都是希望在被缓存的组件再次被渲染的时候做一些事情,针对这种情况,Vue 提供了activated、deactivated两个钩子函数,它们的执行时机分别是KeepAlive包裹的组件被激活时调用和停用时调用。

LRU缓存策略

我们知道keepAlive是有条件的缓存,既然有限制条件,那么旧的组件需要删除缓存,新的组件就需要加入到最新缓存。keepAlive采用了这样一种策略,LRU(Least recently used,最近最少被使用)策略根据数据的历史访问记录来进行淘汰数据,LRU 策略的设计原则是,如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。keepAlive 缓存机制便是根据LRU策略来设置缓存组件新鲜度,当缓存达到上限时将很久未访问的组件从缓存中删除。

举一个例子
  1. 现在缓存最大只允许存3个组件(设置max为3),ABC三个组件依次进入缓存,没有任何问题
  2. 当D组件被访问时,内存空间不足,因为A是最早进入也是最久未被使用的组件,所以A组件从缓存中删除,D组件加入到最新的位置
  3. 当B组件被再次访问时,由于B还在缓存中,B移动到最新的位置,其他组件相应的往后一位
  4. 当E组件被访问时,内存空间不足,C变成最久未使用的组件,C组件从缓存中删除,E组件加入到最新的位置
源码解读
import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'

type VNodeCache = { [key: string]: ?VNode };

// 获取组件名
function getComponentName (opts: ?VNodeComponentOptions): ?string {
  return opts && (opts.Ctor.options.name || opts.tag)
}
// 根据自定义的匹配规则匹配组件是否符合缓存条件
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)
  }
  return false
}
// prune:修剪
function pruneCache (keepAliveInstance: any, filter: Function) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
      const name: ?string = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

const patternTypes: Array<Function> = [String, RegExp, Array]

export default {
  name: 'keep-alive',
  abstract: true, //抽象组件

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },

  created () {
  // 创建缓存对象与keys数组(储所有组件的key)
    this.cache = Object.create(null)
    this.keys = []
  },

  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
  // 观察include & exclude 值改变了就将匹配的组件缓存 如果值更新后有不需要缓存的组件,就将其销毁并清理缓存
    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: VNode = getFirstComponentChild(slot)  // 取第一个
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
  		// 判断是否满足缓存条件
  		// 组件名与 include 不匹配或与 exclude 匹配都会直接退出并返回 VNode ,不走缓存机制
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      const key: ?string = 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]) {
        vnode.componentInstance = cache[key].componentInstance
        // 将key添加到末尾
        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])
  }
}

在组件的开头就将abstract设置为true,代表是该组件是一个抽象组件,不出现在父组件链上是因为在initLifecycle做了处理,判断父级是否为抽象组件,如果是抽象组件,就选取抽象组件的上一级作为父级,忽略与抽象组件和子组件之间的层级关系

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // 确保了在子组件的$parent属性上能访问到父组件实例,在父组件的$children属性上也能访问子组件的实例
  let parent = options.parent
  if (parent && !options.abstract) {  //存在parent&&当前不是抽象组件
    while (parent.$options.abstract && parent.$parent) { // parent是抽象组件&&parent存在parent
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm
}

在created钩子函数中创建了一个对象和数组,分别用于缓存组件和存储所有组件的key

 created () {
  // 创建缓存对象与keys数组(储所有组件的key)
    this.cache = Object.create(null)
    this.keys = []
  },

在KeepAlive销毁的时候调用pruneCacheEntry方法将所有的组件从缓存中清除并销毁,

destroyed () {
   for (const key in this.cache) {
     pruneCacheEntry(this.cache, key, this.keys)
   }
 },
  
 function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

在mounted中,观察include & exclude, 值改变了就将匹配的组件缓存,如果值更新后有不需要缓存的组件,就将其销毁并清理缓存

  mounted () {
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },
  function pruneCache (keepAliveInstance: any, filter: Function) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
      const name: ?string = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

keep-alive 没有编写template 模板,而是由 render 函数决定渲染结果。keep-alive 要求同时只有一个子元素被渲染。所以在开头会获取插槽内的子元素,调用 getFirstComponentChild 获取到第一个子元素的 VNode

接着判断当前组件是否符合缓存条件,组件名与 include 不匹配或与 exclude 匹配都会直接退出并返回 VNode ,不走缓存机制。

如果满足匹配条件就会进入下面的缓存机制的逻辑,如果命中缓存,从 cache 中获取缓存的实例设置到当前的组件上,并调整 key 的位置将其放到最后。如果没命中缓存,将当前 VNode 缓存起来,并加入当前组件的 key 。如果缓存组件的数量超出 max 的值,则调用 pruneCacheEntry 将最旧的组件从缓存中删除,即 keys[0] 的组件。之后将组件的 keepAlive 标记为 true ,最后返回vnode

render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      const key: ?string = vnode.key == null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : ''): vnode.key
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        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])
  }
其实从代码中我们可以看出keep-alive其实渲染的对象是包裹的子组件。

New Vue -> init -> mount -> compile -> render -> patch -> realdom

组件在 patch 过程会会执行createElm,调用 createComponent 来挂载组件的

if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
   return
}
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive 
      if (isDef(i = i.hook) && isDef(i = i.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)  // initComponent
        insert(parentElm, vnode.elm, refElm)  
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

isReactivated 标识组件是否重新激活。在首次加载被KeepAlive包裹的组件时,由KeepAlive中的render函数可知,组件还没有初始化构造完成, componentInstance 还是 undefined 。而组件的 keepAlive 是 true ,因为 KeepAlive作为父级包裹组件,会先于组件挂载,也就是KeepAlive 会先执行 render 的过程,组件被缓存起来,之后对插槽内第一个组件的 keepAlive 赋值为 true ,然后会调用init去初始化组件

init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      //  实例、未被销毁、keepalive
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch 
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
 }

在第一次组件初始化的时候,componentInstance为undefined,所以会执行到else中的方法,

先去执行createComponentInstanceForVnode,其实就是生成了组件的实例并赋值到 componentInstance ,随后调用 $mount 挂载组件,回到 createComponent 中,继续执行下面的逻辑,这时候componentInstance已经存在,调用 initComponent 将 vnode.elm 赋值为真实dom,然后调用 insert 将组件的真实dom插入到父元素中。所以在初始化渲染中,KeepAlive将组件缓存起来,然后正常的渲染组件。

keep-alive包裹的组件是如何使用缓存的呢?

当组件命中缓存被重新激活。再次经历 patch 过程, KeepAlive 是根据插槽获取当前的组件,那么插槽的内容又是如何更新实现缓存的呢

在非初始化渲染时, patch 会调用 patchVnode 对比新旧节点进行更新。在patchVnode中有这样一段操作

let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
}

调用了componentVNodeHooks中的prepatch方法

prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },
//在 lifeCycle文件中
export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  const newScopedSlots = parentVnode.data.scopedSlots
  const oldScopedSlots = vm.$scopedSlots
  const hasDynamicScopedSlot = !!(
    (newScopedSlots && !newScopedSlots.$stable) ||
    (oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
    (newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key)
  )
  // Any static slot children from the parent may have changed during parent's
  // update. Dynamic scoped slots may also have changed. In such cases, a forced
  // update is necessary to ensure correctness.
  const needsForceUpdate = !!(
    renderChildren ||               // has new static slots
    vm.$options._renderChildren ||  // has old static slots
    hasDynamicScopedSlot
  )
  // resolve slots + force update if has children
  if (needsForceUpdate) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }

}

从注释中可以看到 needsForceUpdate 是有插槽才会为 true , keep-alive 符合条件。首先调用 resolveSlots 更新keepAlive 的插槽,然后调用 f o r c e U p d a t e 让 k e e p A l i v e 重 新 渲 染 , 再 走 一 遍 r e n d e r 。 因 为 组 件 在 初 始 化 已 经 缓 存 了 , k e e p − a l i v e 直 接 返 回 缓 存 好 的 A 组 件 V N o d e 。 V N o d e 准 备 好 后 , 又 来 到 了 p a t c h 阶 段 。 这 时 候 组 件 再 次 经 历 c r e a t e C o m p o n e n t 的 过 程 , 调 用 i n i t 。 因 为 c o m p o n e n t I n s t a n c e 已 经 存 在 并 且 k e e p A l i v e 属 性 为 t r u e , 所 以 不 会 再 去 执 行 forceUpdate 让keepAlive重新渲染,再走一遍 render 。因为组件在初始化已经缓存了, keep-alive 直接返回缓存好的A组件 VNode 。VNode 准备好后,又来到了 patch 阶段。这时候组件再次经历 createComponent 的过程,调用 init 。因为componentInstance已经存在并且keepAlive属性为true,所以不会再去执行 forceUpdatekeepAliverenderkeepaliveAVNodeVNodepatchcreateComponentinitcomponentInstancekeepAlivetruemount,所以当组件重新激活的时候不会再执行生命周期钩子,而是走到了prepatch更新实例,回到 createComponent ,此时的 isReactivated 为 true ,调用 reactivateComponent 方法

function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    // 中间省略,最后调用insert方法
    insert(parentElm, vnode.elm, refElm)
  }

在reactivateComponent方法总,调用insert插入组件的dom节点,至此缓存渲染流程完成

function insert (parent, elm, ref) {
    if (isDef(parent)) {
      if (isDef(ref)) {
        if (nodeOps.parentNode(ref) === parent) {
          nodeOps.insertBefore(parent, elm, ref)
        }
      } else {
        nodeOps.appendChild(parent, elm)
      }
    }
  }
总结:

组件首次渲染时,keepAlive会将组件缓存起来。等到缓存的组件重新渲染时,keepAlive 会调用$forceUpdate 重新渲染。这样在 render 时就获取到最新的组件,如果命中缓存则从缓存中返回 VNode 。这样就实现了keepAlive的缓存功能

那么KeepAlive的钩子函数(activated、deactivated)是怎么执行的呢?

上面的渲染流程中已经说过,在组件渲染的时候会调用patch方法,在patch方法的最后会调用invokeInsertHook

invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  function invokeInsertHook (vnode, queue, initial) {
    // delay insert hooks for component root nodes, invoke them after the
    // element is really inserted
    if (isTrue(initial) && isDef(vnode.parent)) {
      vnode.parent.data.pendingInsert = queue
    } else {
      for (let i = 0; i < queue.length; ++i) {
        queue[i].data.hook.insert(queue[i])
      }
    }
  }

在invokeInsertHook里执行会去执行insert,在insert中判断了vnode.data.keepAlive,如果为true执行内部的逻辑,无论怎样最后都会去执行activateChildComponent,最终调用了activated钩子函数

insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },

deactivated钩子也是在patch中调用的

 // 在patch中
 if (isUndef(vnode)) { 
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
 }
 function invokeDestroyHook (vnode) {
    let i, j
    const data = vnode.data
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
      for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
    }
    if (isDef(i = vnode.children)) {
      for (j = 0; j < vnode.children.length; ++j) {
        invokeDestroyHook(vnode.children[j])
      }
    }
  }
  // 在create-component中
  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
  // 在lifecycle中
  export function deactivateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = true
    if (isInInactiveTree(vm)) {
      return
    }
  }
  if (!vm._inactive) {
    vm._inactive = true
    for (let i = 0; i < vm.$children.length; i++) {
      deactivateChildComponent(vm.$children[i])
    }
    callHook(vm, 'deactivated')
  }
}
over!!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值