keep-alive 组件是什么?

<keep-alive>Vue 的内置组件,用来包裹组件,达到缓存组件实例的作用~

Vue 官网文档中提到:

  • <keep-alive> 是一个形象组件:它本身不会渲染一个 DOM 元素,也不会呈现在组件的父组件链中。
  • 当组件在 <keep-alive> 内被切换,它的 activateddeactivated 这两个生命周期钩子函数将会被对应执行。

keep-alive 组件原理

匹配和过滤

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。

初始化

  • created 阶段会初始化两个变量,别离为 cachekeys
  • mounted 阶段会对 includeexclude 变量的值做监测。
created () {    this.cache = Object.create(null)    this.keys = []},mounted () {    this.$watch('include', val => {        pruneCache(this, name => matches(val, name))    })    this.$watch('exclude', val => {        pruneCache(this, name => !matches(val, name))    })},

能够看到,如果 include 或者 exclude 的值发生变化,就会触发 pruneCache 函数,不过筛选的条件须要依据 matches 函数的返回值来决定,所以咱们先来看看它????。

过滤条件

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)  }  /* istanbul ignore next */  return false}

能够看到,pattern 能够有三种取值,别离为:

  • string
  • RegExp
  • Array<string>

之后判断 name 是不是在 pattern 指定的值/范畴内,最初再依据后果返回 true 或者 false

缓存剔除

无论是 include 还是 exclude,他们的值都不是变化无穷的,每当过滤条件扭转,都须要从已有的缓存中「剔除」不符合条件的 key

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)      }    }  }}

能够看到这里又调用了 pruneCacheEntry 来「剔除」不合乎缓存条件的 key????

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)}

这里有个细节:

// 如果以后的 vnode 为空,或者 cached 和 current 不是同一个节点// 就把调用 $destroy 把 cached 销毁if (cached && (!current || cached.tag !== current.tag)) {    cached.componentInstance.$destroy()}

渲染时剔除

下面咱们探讨的是过滤条件的变动状况,如果没变动呢?
其实 <keep-alive> 组件也会在每次渲染的时候都过滤一次组件,保障缓存的组件都曾经通过过滤条件????

render () {    ...    if (        // not included        (include && (!name || !matches(include, name))) ||        // excluded        (exclude && name && matches(exclude, name))      ) {        return vnode      }    ...}

缓存淘汰策略

除了下面说到的两个参数,<keep-alive> 组件还能传一个参数:max

文档中提到一个实例销毁规定:缓存最近拜访的实例,销毁缓存中最久没有被拜访的实例。其实这是一种缓存淘汰策略 —— LRU 算法。

更加具体的规定:

  • 在缓存下限达到之前,不思考销毁实例
  • 对于最新拜访的数据,淘汰的优先级是最低的
  • 对于最不常拜访的数据,淘汰的优先级是最高的

这意味着咱们须要两种数据结构来表白这个算法,其中一种数据结构来存储缓存,另外一种用来存储并示意缓存拜访的新旧水平。

Vue 2.x 与 Vue 3.x 中实现的异同

Vue 2.x 用 cache 来存储缓存,用 keys 来存储缓存中拜访的新旧水平,其中 cache 为对象,keys 为数组。

const { cache, keys } = thisconst 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.keyif (cache[key]) {    vnode.componentInstance = cache[key].componentInstance    // make current key freshest    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)    }}

然而, Vue 3.0 里中采纳的仍然是 LRU 算法,不过用到的 cachekeys 的作用跟 2.0 没有区别,然而数据结构却变了,其中:

  • cache 是用 Map 创立的
  • keys 应用 Set 创立的
const key = vnode.key == null ? comp : vnode.keyconst cachedVNode = cache.get(key)// clone vnode if it's reused because we are going to mutate itif (vnode.el) {    vnode = cloneVNode(vnode)}cache.set(key, vnode)if (cachedVNode) {    // copy over mounted state    vnode.el = cachedVNode.el    vnode.component = cachedVNode.component    if (vnode.transition) {        // recursively update transition hooks on subTree        setTransitionHooks(vnode, vnode.transition!)    }    // avoid vnode being mounted as fresh    vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE    // make this key the freshest    keys.delete(key)    keys.add(key)} else {    keys.add(key)    // prune oldest entry    if (max && keys.size > parseInt(max as string, 10)) {        pruneCacheEntry(Array.from(keys)[0])    }}

显然,在工夫复杂度上 Vue 3.x 要更优,因为 Set 删除某个 key 只须要 $O(1)$ 的工夫复杂度,而数组删除某个 key 却要消耗 $O(n)$ 工夫复杂度。

不过,即便是 Vue 3.x 的写法也还是有点问题的,比方这一段????

pruneCacheEntry(Array.from(keys)[0])

会先将 keys 转成数组再取第一项,复杂度一下子就高了,这里可能是思考到代码量和可读性上的问题,对性能做了取舍,毕竟缓存大量组件的状况也是比拟少见~

扩大 —— 常见的 LRU 算法实现

LRU 算法在实现逻辑的同时,须要满足以下两点:

  1. 删除、增加缓存的工夫复杂度为 $O(1)$
  2. 查找缓存的工夫复杂度为 $O(1)$

其中双向循环链表能够满足条件 1,哈希表能够满足条件 2。基于此咱们能够用哈希表 + 双向循环链表相结合的写法来实现 LRU 算法。以下是实现的代码,基于 LeetCode 上的一道题目:LRU 缓存机制,感兴趣的同学无妨看完之后在下面写写看。

哈希表 + 双向循环链表 实现 LRU 算法

/** * @param {number} capacity */const LRUCache = function (capacity) {    this.map = new Map()    this.doubleLinkList = new DoubleLinkList()    this.maxCapacity = capacity    this.currCapacity = 0};/** * @param {number} val * @param {string} key */const Node = function (val, key) {    this.val = val    this.key = key    this.next = null    this.prev = null}const DoubleLinkList = function () {    this.head = null    this.tail = null}/** * @param {Node} node * 增加节点 */DoubleLinkList.prototype.addNode = function (node) {    if (!this.head) {        this.head = node        this.tail = node        this.head.next = this.tail        this.head.prev = this.tail        this.tail.next = this.head        this.tail.prev = this.head    } else {        const next = this.head        this.head = node        this.head.next = next        this.head.prev = this.tail        this.tail.next = this.head        next.prev = this.head    }}/** * @param {Node} node * @return {void} * 删除双向链表中的节点 */DoubleLinkList.prototype.deleteNode = function (node) {    const prev = node.prev    const next = node.next    prev.next = next    next.prev = prev    // 对头尾节点做非凡判断    if (this.head === node) {        this.head = next    }    if (this.tail === node) {        this.tail = prev    }}/**  * @param {number} key * @return {number} */LRUCache.prototype.get = function(key) {    const { map, doubleLinkList } = this    // 拜访一个 key  的同时,先将它删除,而后再将它置于双向链表的顶部    if (map.has(key)) {        const node = map.get(key)        doubleLinkList.deleteNode(node)        doubleLinkList.addNode(node)        return node.val    }    return -1};/**  * @param {number} key  * @param {number} value * @return {void} */LRUCache.prototype.put = function(key, value) {    const { map, doubleLinkList } = this    // 如果曾经存在这个 key 了    // 那么只有改 key 对应的值    // 而后将 key 对应的节点放到双向链表的头部即可    if (map.has(key)) {        // 先在 map 中找到他        const node = map.get(key)        // 而后将它删掉        doubleLinkList.deleteNode(node)        // 改掉值        node.val = value        // 之后退出到链表头部        doubleLinkList.addNode(node)        map.set(key, node)    } else {        const node = new Node(value, key)        // 如果之前没有这个 key 且缓存已满        if (this.currCapacity === this.maxCapacity) {            // 先将尾巴节点删除            map.delete(doubleLinkList.tail.key)            doubleLinkList.deleteNode(doubleLinkList.tail)            // 而后将新的 key 退出链表和 map            doubleLinkList.addNode(node)            map.set(key, node)        } else {            // 反之缓存没有满            // 间接新的 key 退出链表和 map 即可            doubleLinkList.addNode(node)            map.set(key, node)            // 缓存容量自增            this.currCapacity++        }    }};

参考资料

  • 文档:keep-alive
  • Vue 2.x 源码
  • Vue 3.x 源码