keep-alive实现原理

在上一篇中,介绍了keep-alive的作用以及应用

https://blog.csdn.net/m0_56698268/article/details/129713779

这里稍微看一看它的源码是如何实现的

在源码文件VUE-DEV的src/core/components/keep-alive.js中

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)
        // prune oldest entry
        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 = []
  },

  destroyed () {
    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) {
      // 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
        // 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
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      } else {
        // delay setting the cache until update
        this.vnodeToCache = vnode
        this.keyToCache = key
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}
我们可以发现,它就是和组件的写法是差不多的,但是不同的在于,它是抽象组件
(第3行 abstract : true), 抽象组件的意思就是 keep-alive组件 不会像其他组件一样渲染出DOM节点,但是会渲染出Vnode虚拟节点,所以在Vnode Tree上是可以找到keep-alive的虚拟节点的,DOM Tree上找不到。

接着往下,是它的props属性

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

include和exclude是用于名称匹配的组件是否会被缓存的,max是用于控制可以缓存多少组件实例的,如果一旦超过最大值,会把末尾最后用到的组件进行销毁

然后是一个创建虚拟节点的方法,先不管它。再往下就是五个钩子函数 created、destroyed、mounted、updated、render

  created () {
    this.cache = Object.create(null)
    this.keys = []
  },
创建时,会给实例注入两个属性,cache和keys。
cache是一个对象,里面会存储我们要缓存的组件
keys会储存cache对象的key的集合。
所以,我们就可以通过这两个属性获取到当前缓存的哪些组件

destroyed:销毁的时候,需要把cache对象里面把缓存的组件全部删掉,循环遍历cache里面的值,调用pruneCacheEntry

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

mouted: 逻辑也很简单,就是监听 include和exclude,一旦发生改变,那肯定要重新匹配,过滤下cache

  mounted () {
    this.cacheVNode()
    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 entry: ?CacheEntry = cache[key]
    if (entry) {
      const name: ?string = entry.name
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

  updated () {
    this.cacheVNode()
  },

这个没啥好讲的

重点是这个render,keep-alive实现缓存的核心代码就在这个钩子函数里

  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
        // 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)
            if (this.max && keys.length > parseInt(this.max)) {
                pruneCacheEntry(cache, keys[0], keys, this._vnode)
            }
        }
        vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }

首先是获取到默认插槽里的内容,然后调用getFirstComponentChild方法获取第一个子组件

    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)

假设现在keep-alive组件里的这样子的


    123123
    

那么它获取的应该是componentA组件 (123123并不是组件)

export function getFirstComponentChild (children: ?Array): ?VNode {
  if (Array.isArray(children)) {
    for (let i = 0; i < children.length; i++) {
      const c = children[i]
      if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
        return c
      }
    }
  }
}
这里就是判断c是不是组件,一旦c是组件就返回

接着的逻辑就是 如果vnode存在并获取它的组件选项componentOptions,如果组件选项存在,进入下一步,如果不存在,直接返回vnode或者slot[0]

if (componentOptions) {
    ......
}
return vnode || (slot && slot[0])

所以,直接看组件选项存在后的执行就行了

首先就是取组件的名称去和include和exclude去进行匹配(如果有name的话),如果没有name,就会取tag名。

      // 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
      }
function getComponentName (opts: ?VNodeComponentOptions): ?string {
  return opts && (opts.Ctor.options.name || opts.tag)
}

如果没有匹配上,直接返回这个组件的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
            remove(keys, key)
            keys.push(key)
        } 
        else {
            cache[key] = vnode
            keys.push(key)
            if (this.max && keys.length > parseInt(this.max)) {
                pruneCacheEntry(cache, keys[0], keys, this._vnode)
            }
        }
        vnode.data.keepAlive = true
上面的代码:
首先是看这个vnode有没有key,有的话,直接保存,没有的话,会用“cid::组件的tag”来代替
然后就是看cache中有没有该组件实例,如果有的话,直接取出这个组件实例给vnode

这里的意思是什么呢?比如就是已经缓存了A和B两个组件,此时A是失活状态的,但我切换到A组件时,它reactive了,那么因为已经缓存过了,那我直接取cache里缓存过的这个A直接赋给vnode,就可以实现reactive了。

就这就是把keys这个数组进行一个刷新,具体是怎么操作的呢?

其实用到的是操作系统里内存管理的一个LRU置换算法,全称叫做 Least Recently Used,叫做最近最少使用。他会根据数据的历史访问记录来淘汰数据,这种算法的核心思想怎么来的呢? 它是这么认为的,如果某一个数据最近被访问过了,它会认为它将来被访问的几率就会变高,而如果有的数据最近很少访问或者没有访问,那将来访问它的几率就不会高

用一个简单的例子解释一下,现在有一个cache,但是它只有三个块,也就是只能装下3个页面,而我现在要依次访问如下的页面: 页面1、页面2、页面3、页面1、页面4

keep-alive实现原理_第1张图片
一开始,前三个页面可以按照顺序依次存在放块A、B、C中,而到了第四个页面为页面1,首先它会查看页面1在不在块内,发现确实在,在块A中。OK,那我就不用置换掉其他页面了,此时会调整访问顺序,之前的顺序是 1、2、3的顺序访问,第四个页面进来以后,他们访问的先后顺序就调整为了 2、3、1 (也就代表页面1是最近才被访问的)。
那再接着,页面4要被访问了,首先看cache里有页面4吗? 没有! 那我需要先置换出一个页面,空出位置给我的页面4。 那要置换掉谁呢? 就看谁是LRU了,按照上面更改后的访问顺序(2、3、1),发现页面2是最久远被访问的,所以置换掉页面2,从而放入页面4。同样的,依旧会刷新他们的访问顺序,现在就是(3、1、4)了

再次基础上,如果下一个要访问的页面是页面5呢?存放的块是哪个?且置换后的顺序为?

存在在块C中,顺序为(1、4、5)

这就是简单的LRU算法的小例子,但是这种算法有一个缺点会导致页面的抖动(当然,这里就不要求掌握了,感兴趣的可以去看看其他内存管理的置换算法)

以上的例子就能很好地解释源码中的if else的逻辑。

最后标识vnode.data.keepAlive=true

Last But Not Least,组件一旦被keep-alive缓存,那么再次渲染时就不会执行created、mouted、destroyed钩子,使用keep-alive组件后,被缓存的组件生命周期会多activated和deactivated两个钩子函数。

你可能感兴趣的:(前端,vue.js,前端,javascript)