vue抽象组件 keep-alive

438 阅读5分钟

keep-alive是一个内置抽象组件(它自身不会渲染一个 DOM 元素,也不会出现在父组件链中,abstract: true 属性值决定)。包裹动态组件时,会缓存不活动的组件实例。而不是销毁它们。 其主要用于保留组件状态(保留在内存中),避免重新渲染

常用场景

  • 从列表进入详情页面,再回到列表页面,不需要刷新列表页面
  • 填写表单,进入下一步,返回上一步数据不会清空
  • 长列表内容

使用语法

有三个属性Props:

  • include 字符串或正则表达式。只有名称匹配的组件会被缓存。
  • exclude 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
  • max 最多可以缓存多少组件实例。超出上限使用LRU的策略置换缓存数据(选择最近最久未使用的页面予以淘汰)
// include="xxx,xx" xxx 是组件的名字
 <keep-alive :include="whiteList" :exclude="blackList" :max="number">
    // 1、动态组件中使用
    <component :is="currentComponent"></component>
    // 2、动态路由中使用
    <router-view></router-view>
  </keep-alive>

结合router 使用

<keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
export default new Router({
  routes: [
    {
      path: '/xx',
      name: 'Hello',
      component: Hello,
      meta: {
        keepAlive: true // false不需要缓存, true 需要
      }
    }
  ]

tips: 有时需要特定情况下keep-alive,则需要利用router的钩子函数beforeRouteEnter,beforeRouteLeave 进出页面,进行处理。 参考

删除keep-alive的方法

keep-alive的生命周期

当引入keep-alive的时,钩子的触发顺序:created -> mounted-> activated,退出时触发deactivated。当再次进入(前进或者后退)时,只触发activated。销毁逻辑不会执行。业务中可新增对应的生命周期函数使用。

keep-alive原理

源码地址 src/core/components/keep-alive.js

// src/core/components/keep-alive.js
export default {
  name: 'keep-alive',
  abstract: true, // 判断当前组件虚拟dom是否渲染成真是dom的关键
  props: {
    include: patternTypes, // 缓存白名单
    exclude: patternTypes, // 缓存黑名单
    max: [String, Number] // 缓存的组件实例数量上限
  },

  created () {
    this.cache = Object.create(null) // 缓存虚拟dom
    this.keys = [] // 缓存的虚拟dom的健集合
  },

  destroyed () {
    for (const key in this.cache) { // 删除所有的缓存
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
    // 实时监听黑白名单的变动
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render () {
    // 先省略...
  }
}

render 函数

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 // 定义组件的缓存key
        // 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) // 调整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对象包括的第一个子组件对象
  • 根据白黑名单是否匹配返回本身的vnode
  • 根据vnode的cid和tag生成的key,在缓存对象中是否有当前缓存,如果有则返回,并更新key在keys中的位置
  • 如果当前缓存对象不存在缓存,就往cache添加这个的内容,并且根据LRU算法删除最近没有使用的实例
  • 设置为第一个子组件对象的keep-alive为true

面试

  • 1、keep-alive,它不会生成真正的DOM节点,这是怎么做到的? 在keep-alive中,设置了abstract: true,那Vue就会跳过该组件实例。 最后构建的组件树中就不会包含keep-alive组件
// src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
  const options = vm.$options
  // 找到第一个非abstract的父组件实例
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
  vm.$parent = parent
  // ...
}

  • 2、说说keep-alive 场景=》属性 =》生命周期=》 源码

  • 3、keep-alive包裹的组件是如何使用缓存的? 在patch阶段,会执行createComponent函数:

// src/core/vdom/patch.js
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 */)
      }


      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm) // 将缓存的DOM(vnode.elm)插入父元素中
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

在首次加载被包裹组件时,由keep-alive.js中的render函数可知,vnode.componentInstance的值是undefined,keepAlive的值是true,因为keep-alive组件作为父组件,它的render函数会先于被包裹组件执行;那么就只执行到i(vnode, false),后面的逻辑不再执行; 再次访问被包裹组件时,vnode.componentInstance的值就是已经缓存的组件实例,那么会执行insert(parentElm, vnode.elm, refElm)逻辑,这样就直接把上一次的DOM插入到了父元素中。

  • 4、为什么keep-alive包裹的组件钩子函数只执行一次? 被缓存的组件实例会为其设置keepAlive = true,而在初始化组件钩子函数中,进行了判断 详细查看:

  • LRU 算法 利用set处理

 * 1. 现判定是否存在 若存在则将其放置最新位置 
 * 2. 存入cache 
 * 3. 判定插入后是否超出容器体积 若超出则移除首位
 // 最后一个权重最大
class LRUCache {
    constructor(capacity) {
        this.cache = new Map()
        this.capacity = capacity
    }
    get(k) {
    	// 是否有
        if (!this.cache.has(k)) return -1
        const v = this.cache.get(k)
        // make sure it is latest 
        this.cache.delete(k)
        this.cache.set(k, v)
        return v
    }
    put(k, v) {
       // delete if if it exists
       if (this.cache.has(k)) this.cache.delete(k)

       // store it in cache
       this.cache.set(k, v)

       // make sure not to exceed the range after store it in cache
       // 大于长度  删除
       if (this.cache.size > this.capacity) {
           const first = this.cache.keys().next().value
           this.cache.delete(first)
       }
    }
}

利用哈希表+链表处理,实现操作是O(1).

伪代码:

  • 初始化头尾两个节点
  • moveToHead(node) 读取的时候将节点移到head
  • addToHead(node) 新增节点时,将其移到最前面
  • removeFromList 将节点从list上移出
  • removeLRUItem 删除头节点 当level 长度够的时候
class ListNode {
	constructor(key, val){
    	this.key = key
        this.value = val
        this.next = null
        this.prev = null
    }
}
class LRUCache {
    constructor(level){
    	this.level = level
        this.hash = {}
        this.count = 0
        this.vHead = new ListNode()
        this.vTail = new ListNode()
        this.vHead.next = this.vTail
        this.vTail.prev = this.vHead
    }
    get(key) {
      let node = this.hash[key]
      if (node == null) return -1
      this.moveToHead(node)
      return node.value
    }
  put(key, value) {
    let node = this.hash[key]
    if (node == null) {
      if (this.count == this.level) {
        this.removeLRUItem()
      }
      let newNode = new ListNode(key, value)
      this.hash[key] = newNode
      this.addToHead(newNode)
      this.count++
    } else {
      node.value = value
      this.moveToHead(node)
    }
  }
 moveToHead(node) {
    this.removeFromList(node)
    this.addToHead(node)
  }
  removeFromList(node) {
    let temp1 = node.prev
    let temp2 = node.next
    temp1.next = temp2
    temp2.prev = temp1
  }
  addToHead(node) {
    node.prev = this.vHead
    node.next = this.vHead.next
    this.vHead.next.prev = node
    this.vHead.next = node
  }
  removeLRUItem() {
    let tail = this.popTail()
    delete this.hash[tail.key]
    this.count--
  }
  popTail() {
    let tail = this.vTail.prev
    this.removeFromList(tail)
    return tail
  }
}

keep-alive原理

keep-alive实现原理

Vue keep-alive实践