【vue2.x原理剖析十一】keep-alive原理

161 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情

前言

源码分析文章看了很多,也阅读了至少两遍源码。终归还是想自己写写,作为自己的一种记录和学习。重点看注释部分和总结,其余不用太关心,通过总结对照源码回看过程和注释收获更大

作用

使用keep-alive包裹动态组件时,会对组件进行缓存,避免组件的重新渲染

用法

  • 动态组件
<keep-alive :include="whiteList" :exclude="blackList" :max="count">
  <component :is="component"></component>
</keep-alive>
  • keep-alive
<keep-alive :include="whiteList" :exclude="blackList" :max="count">
  <router-view></router-view>
</keep-alive>

初始化

在初始化全局api时,会构建keep-alive

// src/core/global-api
import builtInComponents from '../components/index'
...
extend(Vue.options.components, builtInComponents)

定义

如果有缓存,使用lru算法(最近最久未使用):将组件先删除再添加进去,例如有a,b,c,d四个组件,用c时,先将c删除,再添加到d后边

export default {
  name: 'keep-alive',
  abstract: true,
  props: {
    include: patternTypes,// 白名单
    exclude: patternTypes, // 黑名单
    max: [String, Number]// 缓存的最大个数
  },
  methods: {
    cacheVNode() {
      const { cache, keys, vnodeToCache, keyToCache } = this
      if (vnodeToCache) {
        const { tag, componentInstance, componentOptions } = vnodeToCache
        // 缓存组件
        cache[keyToCache] = {
          name: getComponentName(componentOptions),tag,componentInstance,
        }
        keys.push(keyToCache)
        // 超过最大限制删除第一个
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
        this.vnodeToCache = null
      }
    }
  },
  created () {
    this.cache = Object.create(null) // 缓存列表
    this.keys = [] // 缓存的key列表 缓存组件实例
  },
  destroyed () { // keep-alive销毁时,删除所有缓存
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },
  mounted () { // 监控缓存列表
    this.cacheVNode()
    this.$watch('include', val => { // 缓存列表可以是动态的
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },
  updated () {
    this.cacheVNode()
  },
  render () {
    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 (// 获取组件名 看是否需要缓存 不需要则直接返回
        (include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        return vnode // 不需要缓存 直接返回虚拟节点
      }
      const { cache, keys } = this
      // 缓存的key为组件的id+标签名
      const key: ?string = vnode.key == null 
      ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      //如果有key 将组件实例直接复用
      if (cache[key]) { 
        vnode.componentInstance = cache[key].componentInstance
        // lru算法
        remove(keys, key)
        keys.push(key)
      } else {
        this.vnodeToCache = vnode
        this.keyToCache = key
      }
      // 组件被keep-alive 防止组件在初始化时候重新init
      vnode.data.keepAlive = true
    }
    // 先渲染 当前对应的组件内容 返回组件的虚拟节点 之后将节点初始化时跳过渲染流程,不执行init 会执行activated 和 deactivated钩子(可做拉取最新数据的操作)
    return vnode || (slot && slot[0])
  }
}

总结

keep-alive的用法有两种,一种是动态组件,另一种是router-view,由于keep-alive只处理第一个子元素,所以一般这两种方式搭配使用.可以通过includeexclude设置白名单和黑名单,通过max设置缓存的个数。vue在渲染时会执行patch,而组件在patch过程中会执行createComponent方法,初次渲染时,父组件keep-aliverender函数会先执行,会将组件vnode存在缓存中并设置data.keepAlivetrue,但是此时还么有组件实例,所以会正常执行init函数并执行组件的mount,之后会缓存了vnode创建生成的DOM节点,所以对于初次渲染,keep-alive建立缓存之外,和普通组件渲染没什么区别。当切换组件,就会命中缓存,在创建组件时会定义钩子函数,例如init、prepatch等,在diff之前,会执行prepatch钩子函数,主要是去更新组件实例的一些属性,由于keep-alive组件本质支持了slot,所以再执行prepatch时候,需要对自己的children做重新解析。并触发keep-alive组件实例的$forceUpdate逻辑,也就是会重新执行keep-aliverender方法。再次渲染子组件时,由于有缓存并且data.keepAlivetrue,所以不会再走init方法,将缓存的DOM对象直接插入到目标元素中,完成渲染过程