如果需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组件包裹需要保存的组件。
    (1)keep-alive
    keep-alive有以下三个属性:

    • include 字符串或正则表达式,只有名称匹配的组件会被匹配;
    • exclude 字符串或正则表达式,任何名称匹配的组件都不会被缓存;
    • max 数字,最多可以缓存多少组件实例。

    注意:keep-alive 包裹动态组件时,会缓存不活动的组件实例。

    主要流程

    1. 判断组件 name ,不在 include 或者在 exclude 中,直接返回 vnode,说明该组件不被缓存。
    2. 获取组件实例 key ,如果有获取实例的 key,否则重新生成。
    3. key生成规则,cid +”∶∶”+ tag ,仅靠cid是不够的,因为相同的构造函数可以注册为不同的本地组件。
    4. 如果缓存对象内存在,则直接从缓存对象中获取组件实例给 vnode ,不存在则添加到缓存对象中。 5.最大缓存数量,当缓存组件数量超过 max 值时,清除 keys 数组内第一个组件。

    (2)keep-alive 的实现

    1. const patternTypes: Array<Function> = [String, RegExp, Array] // 接收:字符串,正则,数组
    2. export default {
    3. name: 'keep-alive',
    4. abstract: true, // 抽象组件,是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。
    5. props: {
    6. include: patternTypes, // 匹配的组件,缓存
    7. exclude: patternTypes, // 不去匹配的组件,不缓存
    8. max: [String, Number], // 缓存组件的最大实例数量, 由于缓存的是组件实例(vnode),数量过多的时候,会占用过多的内存,可以用max指定上限
    9. },
    10. created() {
    11. // 用于初始化缓存虚拟DOM数组和vnode的key
    12. this.cache = Object.create(null)
    13. this.keys = []
    14. },
    15. destroyed() {
    16. // 销毁缓存cache的组件实例
    17. for (const key in this.cache) {
    18. pruneCacheEntry(this.cache, key, this.keys)
    19. }
    20. },
    21. mounted() {
    22. // prune 削减精简[v.]
    23. // 去监控include和exclude的改变,根据最新的include和exclude的内容,来实时削减缓存的组件的内容
    24. this.$watch('include', (val) => {
    25. pruneCache(this, (name) => matches(val, name))
    26. })
    27. this.$watch('exclude', (val) => {
    28. pruneCache(this, (name) => !matches(val, name))
    29. })
    30. },
    31. }

    render函数:

    1. 会在 keep-alive 组件内部去写自己的内容,所以可以去获取默认 slot 的内容,然后根据这个去获取组件
    2. keep-alive 只对第一个组件有效,所以获取第一个子组件。
    3. 和 keep-alive 搭配使用的一般有:动态组件 和router-view

      1. render () {
      2. //
      3. function getFirstComponentChild (children: ?Array<VNode>): ?VNode {
      4. if (Array.isArray(children)) {
      5. for (let i = 0; i < children.length; i++) {
      6. const c = children[i]
      7. if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
      8. return c
      9. }
      10. }
      11. }
      12. }
      13. const slot = this.$slots.default // 获取默认插槽
      14. const vnode: VNode = getFirstComponentChild(slot)// 获取第一个子组件
      15. const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions // 组件参数
      16. if (componentOptions) { // 是否有组件参数
      17. // check pattern
      18. const name: ?string = getComponentName(componentOptions) // 获取组件名
      19. const { include, exclude } = this
      20. if (
      21. // not included
      22. (include && (!name || !matches(include, name))) ||
      23. // excluded
      24. (exclude && name && matches(exclude, name))
      25. ) {
      26. // 如果不匹配当前组件的名字和include以及exclude
      27. // 那么直接返回组件的实例
      28. return vnode
      29. }
      30. const { cache, keys } = this
      31. // 获取这个组件的key
      32. const key: ?string = vnode.key == null
      33. // same constructor may get registered as different local components
      34. // so cid alone is not enough (#3269)
      35. ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
      36. : vnode.key
      37. if (cache[key]) {
      38. // LRU缓存策略执行
      39. vnode.componentInstance = cache[key].componentInstance // 组件初次渲染的时候componentInstance为undefined
      40. // make current key freshest
      41. remove(keys, key)
      42. keys.push(key)
      43. // 根据LRU缓存策略执行,将key从原来的位置移除,然后将这个key值放到最后面
      44. } else {
      45. // 在缓存列表里面没有的话,则加入,同时判断当前加入之后,是否超过了max所设定的范围,如果是,则去除
      46. // 使用时间间隔最长的一个
      47. cache[key] = vnode
      48. keys.push(key)
      49. // prune oldest entry
      50. if (this.max && keys.length > parseInt(this.max)) {
      51. pruneCacheEntry(cache, keys[0], keys, this._vnode)
      52. }
      53. }
      54. // 将组件的keepAlive属性设置为true
      55. vnode.data.keepAlive = true // 作用:判断是否要执行组件的created、mounted生命周期函数
      56. }
      57. return vnode || (slot && slot[0])
      58. }

      keep-alive 具体是通过 cache 数组缓存所有组件的 vnode 实例。当 cache 内原有组件被使用时会将该组件 key 从 keys 数组中删除,然后 push 到 keys数组最后,以便清除最不常用组件。

    实现步骤:

    1. 获取 keep-alive 下第一个子组件的实例对象,通过他去获取这个组件的组件名
    2. 通过当前组件名去匹配原来 include 和 exclude,判断当前组件是否需要缓存,不需要缓存,直接返回当前组件的实例vNode
    3. 需要缓存,判断他当前是否在缓存数组里面:
    • 存在,则将他原来位置上的 key 给移除,同时将这个组件的 key 放到数组最后面(LRU)
    • 不存在,将组件 key 放入数组,然后判断当前 key数组是否超过 max 所设置的范围,超过,那么削减未使用时间最长的一个组件的 key
    1. 最后将这个组件的 keepAlive 设置为 true

    (3)keep-alive 本身的创建过程和 patch 过程
    缓存渲染的时候,会根据 vnode.componentInstance(首次渲染 vnode.componentInstance 为 undefined) 和 keepAlive 属性判断不会执行组件的 created、mounted 等钩子函数,而是对缓存的组件执行 patch 过程∶ 直接把缓存的 DOM 对象直接插入到目标元素中,完成了数据更新的情况下的渲染过程。

    首次渲染

    • 组件的首次渲染∶判断组件的 abstract 属性,才往父组件里面挂载 DOM

      1. // core/instance/lifecycle
      2. function initLifecycle (vm: Component) {
      3. const options = vm.$options
      4. // locate first non-abstract parent
      5. let parent = options.parent
      6. if (parent && !options.abstract) { // 判断组件的abstract属性,才往父组件里面挂载DOM
      7. while (parent.$options.abstract && parent.$parent) {
      8. parent = parent.$parent
      9. }
      10. parent.$children.push(vm)
      11. }
      12. vm.$parent = parent
      13. vm.$root = parent ? parent.$root : vm
      14. vm.$children = []
      15. vm.$refs = {}
      16. vm._watcher = null
      17. vm._inactive = null
      18. vm._directInactive = false
      19. vm._isMounted = false
      20. vm._isDestroyed = false
      21. vm._isBeingDestroyed = false
      22. }
    • 判断当前 keepAlive 和 componentInstance 是否存在来判断是否要执行组件 prepatch 还是执行创建 componentlnstance

      1. // core/vdom/create-component
      2. init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
      3. if (
      4. vnode.componentInstance &&
      5. !vnode.componentInstance._isDestroyed &&
      6. vnode.data.keepAlive
      7. ) { // componentInstance在初次是undefined!!!
      8. // kept-alive components, treat as a patch
      9. const mountedNode: any = vnode // work around flow
      10. componentVNodeHooks.prepatch(mountedNode, mountedNode) // prepatch函数执行的是组件更新的过程
      11. } else {
      12. const child = vnode.componentInstance = createComponentInstanceForVnode(
      13. vnode,
      14. activeInstance
      15. )
      16. child.$mount(hydrating ? vnode.elm : undefined, hydrating)
      17. }
      18. },

      prepatch 操作就不会在执行组件的 mounted 和 created 生命周期函数,而是直接将 DOM 插入

    (4)LRU (least recently used)缓存策略
    LRU 缓存策略∶ 从内存中找出最久未使用的数据并置换新的数据。
    LRU(Least rencently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。 最常见的实现是使用一个链表保存缓存数据,详细算法实现如下∶

    • 新数据插入到链表头部
    • 每当缓存命中(即缓存数据被访问),则将数据移到链表头部
    • 链表满的时候,将链表尾部的数据丢弃。