likes
comments
collection

vue2源码分析-keep-alive组件

作者站长头像
站长
· 阅读数 10

本文正在参加「金石计划 . 瓜分6万现金大奖」

简介

keep-aliveVue.js的一个内置组件。它能够将指定的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。

具体用法咱们这里就不再细说了,今天主要是探讨它的源码。

本文vue版本为2.6.14

源码概览

我们先来看看 keep-alive 组件整体的源码。

export default {
  name: 'keep-alive',
  abstract: true, // 定义为抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。

  props: {
    include: patternTypes, // 需要缓存组件,支持字符串、正则表达式或一个数组
    exclude: patternTypes, // 不需要缓存组件,支持字符串、正则表达式或一个数组
    max: [String, Number] // 最大缓存个数
  },

  methods: {
    // 添加vnode和key到cache对象和keys数组,也就是缓存组件。
    cacheVNode() {
      const { cache, keys, vnodeToCache, keyToCache } = this
      if (vnodeToCache) {
        const { tag, componentInstance, componentOptions } = vnodeToCache
        cache[keyToCache] = {
          name: getComponentName(componentOptions),
          tag,
          componentInstance,
        }
        keys.push(keyToCache)
        // LRU算法,如果触达max,则将最近最少使用的数组顶元素删除。
        if (this.max && keys.length > parseInt(this.max)) {
          // 简单理解,从缓存对象中删除某个组件
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
        this.vnodeToCache = null
      }
    }
  },

  /* created钩子中初始化cache对象和keys数组 */
  created () {
    this.cache = Object.create(null) // 缓存对象
    this.keys = [] // 缓存key数组
  },

  /* destroyed钩子中销毁所有cache中的组件实例 */
  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  // 挂载时
  mounted () {
    this.cacheVNode()
    // 对include和exclude进行监听
    // 发现缓存的节点名称和新的规则没有匹配上的时候,就把这个缓存节点从缓存中摘除。
    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
    // 获取第一个子元素的 vnode
    const vnode: VNode = getFirstComponentChild(slot)
    // 获取组件options,以便获取到name
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // 首先获取组件的name,没有name则获取组件标签名
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      // 如果没匹配上,则什么都不处理,直接返会组件的vnode,否则的话走缓存逻辑
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      // 获取组件的key,没有key则通过组件cid和tag组合生成
      const key: ?string = vnode.key == null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      // 如果之前缓存过,则直接从缓存中获取
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        // 确保是最新的。(LRU算法,最近最少使用)
        // 首先将该位置的key移除,然后将该key添加到数组末尾。
        // 这样的好处就是,当触达到max的时候将数组前面的踢出就可以了。
        remove(keys, key)
        keys.push(key)
      } else {
        // 否则进行缓存,不过是延迟到update再添加到缓存中
        this.vnodeToCache = vnode
        this.keyToCache = key
      }

      // 添加keepAlive标记
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}

下面我们来逐步分析。

created钩子

created钩子会创建一个cache对象,用来作为缓存容器,保存vnode节点。创建一个keys数组,用来保存缓存的key

/* created钩子中初始化cache对象和keys数组 */
created () {
  this.cache = Object.create(null) // 缓存对象
  this.keys = [] // 缓存key数组
},

destroyed钩子

destroyed钩子则在组件被销毁的时候遍历cache对象,来清除cache缓存中的所有组件实例。并且keys数组也会跟着清空。

/* destroyed钩子中销毁所有cache中的组件实例 */
destroyed () {
  for (const key in this.cache) {
    pruneCacheEntry(this.cache, key, this.keys)
  }
},

我们再来看看pruneCacheEntry方法。简单理解就是将cache对象中某key对应的vnode移除,keys数组中某key删除。

function pruneCacheEntry (
  cache: CacheEntryMap,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  // 通过key获取缓存组件
  const entry: ?CacheEntry = cache[key]
  // 组件销毁
  if (entry && (!current || entry.tag !== current.tag)) {
    entry.componentInstance.$destroy()
  }
  // cache对象该key置空
  cache[key] = null
  // keys数组,删除该key
  remove(keys, key)
}

接下来我们再来看看render函数。

render

render () {
  // 获取默认插槽
  const slot = this.$slots.default
  // 获取第一个子元素的 vnode
  const vnode: VNode = getFirstComponentChild(slot)
  // 获取组件options,以便获取到name
  const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
  if (componentOptions) {
    // 首先获取组件的name,没有name则获取组件标签名
    const name: ?string = getComponentName(componentOptions)
    const { include, exclude } = this
    // 如果没匹配上,则什么都不处理,直接返会组件的vnode,否则的话走缓存逻辑
    if (
      // not included
      (include && (!name || !matches(include, name))) ||
      // excluded
      (exclude && name && matches(exclude, name))
    ) {
      return vnode
    }

    const { cache, keys } = this
    // 获取组件的key,没有key则通过组件cid和tag组合生成
    const key: ?string = vnode.key == null
      ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
      : vnode.key
    // 如果之前缓存过,则直接从缓存中获取
    if (cache[key]) {
      vnode.componentInstance = cache[key].componentInstance
      // 确保是最新的。(LRU算法,最近最少使用)
      // 首先将该位置的key移除,然后将该key添加到数组末尾。
      // 这样的好处就是,当触达到max的时候将数组前面的踢出就可以了。
      remove(keys, key)
      keys.push(key)
    } else {
      // 否则进行缓存,不过是延迟到update再添加到缓存中
      this.vnodeToCache = vnode
      this.keyToCache = key
    }

    // 添加keepAlive标记
    vnode.data.keepAlive = true
  }
  return vnode || (slot && slot[0])
}

render函数的逻辑也很清晰。

首先通过getFirstComponentChild获取第一个子组件,获取该组件的name(存在组件名则直接使用组件名,否则会使用tag)。

接下来会将这个name通过includeexclude属性进行匹配,匹配不成功(说明不需要进行缓存)则不进行任何操作直接返回vnode,否则走缓存逻辑。

要查找缓存,首先需要获取keykey的话优先获取组件的key,组件没有key则通过组件cidtag组合生成一个key

接下来的事情很简单,根据keythis.cache中查找,如果存在则说明之前已经缓存过了,直接将缓存的vnodecomponentInstance(组件实例)覆盖到目前的vnode上面。否则将vnode存储在cache中(不是实时的,而是在update方法中修改)。

然后添加keepAlive标记,这个标记有什么用呢?我们知道被keep-alive包裹的组件后面的渲染是不会触发createdmounted生命周期函数,而是会触发acitvateddeactivated生命周期函数。这个标记就是用来做触发生命周期函数的判断的。

最后返回vnode(有缓存时该vnodecomponentInstance已经被替换成缓存中的了)。

mounted钩子

mounted里面做了两件事情。

首先初始化的时候,如果组件是需要缓存的话则会进行缓存。

然后对includeexclude进行监听,当这两个值发生变化的时候,则会重新计算cachekeys中缓存的数据。

mounted () {
  // 如果有需要缓存的组件,则进行缓存
  this.cacheVNode()
  // 对include和exclude进行监听
  // 发现缓存的节点名称和新的规则没有匹配上的时候,就把这个缓存节点从缓存中摘除。
  this.$watch('include', val => {
    pruneCache(this, name => matches(val, name))
  })
  this.$watch('exclude', val => {
    pruneCache(this, name => !matches(val, name))
  })
},

pruneCache方法主要是过滤目前缓存的vnodecache对象和缓存keykeys数组。并将不符合新规则的vnodekey进行移除。

function pruneCache (keepAliveInstance: any, filter: Function) {
  const { cache, keys, _vnode } = keepAliveInstance
  // 遍历cache对象
  for (const key in cache) {
    // 获取缓存组件
    const entry: ?CacheEntry = cache[key]
    if (entry) {
      const name: ?string = entry.name
      // 如果之前缓存的组件并不符合新的规则,则进行移除
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

我们再来看看update钩子

updated钩子

update方法也是用来将组件添加进缓存。

因为之前render方法中,对需要缓存的组件而没有缓存的话会修改this.vnodeToCachethis.keyToCache的值(并不是直接缓存),进而会触发update方法。也就是延迟缓存。

updated () {
  this.cacheVNode()
},

总结

keep-alive组件的缓存是基于VNode节点的而不是直接存储DOM结构。它将满足条件的组件在cache对象中缓存起来,在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染。并且使用到了LRU算法(最近最少使用),每次触发缓存,就会将该缓存组件放到数组末尾,当触达到max后,会将最近最少使用的缓存组件进行剔除。

系列文章

vue2源码分析-data、props、methods、computed属性可以重名吗

vue2源码分析-响应式原理

vue2源码分析-依赖收集

vue2源码分析-派发更新

vue2源码分析-VNode和diff算法

后记

感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!