likes
comments
collection
share

[Vue 源码]keep-alive 组件逻辑分析(下)

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

前言

阅读本章节前建议先查阅一下两章节

[Vue 源码]keep-alive 组件逻辑分析(上)

[Vue 源码]keep-alive 组件逻辑分析(中)

本章节主要对 keep-alive 组件的生命周期进行一个分析,组件的创建、销毁、挂载等;除此之外,对 keep-alive 所使用到的缓存算法进行一个简要分析, 从而对 keep-alive 在内存使用上的优化策略有一定的了解

生命周期

keep-alive 包裹的子组件在再次渲染时,并不会执行 mounted 生命周期钩子,只会执行 activated 钩子。而当子组件被掩藏时,也不会执行 destroyed 生命周期钩子,而是执行 deactivated 钩子

假设现在 keep-alive 包裹的动态组件中,可以在 child1child2 两个组件之间进行切换,那么当从 child1 切换到 child2 时, child1 组件会执行 deactivated 钩子, 当从 child2 再次切回 child1 时,会执行 child2deactivated ,然后执行 child1activated 钩子

deactivated

先从组件销毁说起,当从 child1 切换到 child2 时, child1 会执行 deactivated 钩子而不是 destroyed 钩子。在前面分析 patch 过程中会对新旧节点的改变进行对比,从而尽可能范围小的去操作真实 DOM ,当 diff 完成对节点操作之后,接下来还有一个重要的步骤就是对旧的组件执行销毁移除操作。

function patch (oldVnode, vnode, hydrating, removeOnly) {
  // destroy old node
  if (isDef(parentElm)) {
    removeVnodes(parentElm, [oldVnode], 0, 0)
  } else if (isDef(oldVnode.tag)) {
    invokeDestroyHook(oldVnode)
  }
}

function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
  for (; startIdx <= endIdx; ++startIdx) {
    // 拿到需要移除的组件
    const ch = vnodes[startIdx]
    if (isDef(ch)) {
      if (isDef(ch.tag)) {
        // 真实节点的移除操作
        removeAndInvokeRemoveHook(ch)
        invokeDestroyHook(ch)
      } else { // Text node
        removeNode(ch.elm)
      }
    }
  }
}

removeAndInvokeRemoveHook 会对旧的节点进行移除操作,其中关键的一步就是会将真实节点从父元素中删除。 invokeDestroyHook 是销毁组件钩子的核心,如果该组件下存在子组件,会递归去调用 invokeDestroyHook 执行销毁操作。销毁过程会执行组件内部定义的 destroy 钩子

function invokeDestroyHook (vnode) {
  let i, j
  const data = vnode.data
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
    // 执行组件内部的 destroy 钩子函数
    for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
  }
  // 如果组件存在子组件,则遍历子组件递归调用 invokeDestroyHook 执行钩子
  if (isDef(i = vnode.children)) {
    for (j = 0; j < vnode.children.length; ++j) {
      invokeDestroyHook(vnode.children[j])
    }
  }
}

前面已经分析了组件 init prePatch 两个内部钩子,接下来看看 destroy 钩子函数

const componentVNodeHooks = {
  destroy (vnode: MountedComponentVNode) {

    // 获取到组件实例
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        // 如果不是 keep-alive 组件,则执行销毁操作
        componentInstance.$destroy()
      } else {
        // 如果是已经缓存的组件
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}

当组件是 keep-alive 缓存过的组件时, 即用 keepAlive 标记过的组件,则不会执行实例的销毁了,即 componentInstance.$destroy() 过程。 $destroy() 过程会做一些列的组件销毁操作,其中 beforeDestroydestroyed 钩子函数也是在 $destroy 过程中进行调用。而 deactivateChildComponent 的处理过程则和 $destroy 完全不同。

export function deactivateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = true
    if (isInInactiveTree(vm)) {
      return
    }
  }
  if (!vm._inactive) {
    // 标记组件已经被停用
    vm._inactive = true
    for (let i = 0; i < vm.$children.length; i++) {
      // 存在子组件时,递归调用。
      deactivateChildComponent(vm.$children[i])
    }
    // 调用 deactivated 钩子
    callHook(vm, 'deactivated')
  }
}

_directInactive 用来标记这个被停用的组件是否时最顶层的组件。而 _inactive 是停用的标记,同样子组件也需要递归去调用 deactivateChildComponent ,打上停用标记。最终会执行用户定义的 deactivated 钩子函数

activated

同样是在 patch 过程中, 当旧组件移除并销毁或停用之后,对新的组件也会执行相应的钩子。这也是停用的钩子会比启用的钩子先执行的原因。

function patch (oldVnode, vnode, hydrating, removeOnly) {
  {
    // 销毁或停用节点
    if (isDef(parentElm)) {
      removeVnodes(parentElm, [oldVnode], 0, 0)
    } else if (isDef(oldVnode.tag)) {
      invokeDestroyHook(oldVnode)
    }
  }

  //   插入节点
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

function invokeInsertHook (vnode, queue, initial) {
  // delay insert hooks for component root nodes, invoke them after the
  // element is really inserted
  // 当节点已经被插入是,会延迟执行 insert 钩子
  if (isTrue(initial) && isDef(vnode.parent)) {
    vnode.parent.data.pendingInsert = queue
  } else {
    for (let i = 0; i < queue.length; ++i) {
      // 调用组件内部的 insert 钩子函数
      queue[i].data.hook.insert(queue[i])
    }
  }
}

来看下组件的 insert 钩子函数的具体实现

const componentVNodeHooks = {
  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },
}

在第一次实例化组件时,由于组件的 _isMounted 属性不存在,所以会调用 mounted 钩子函数,当从 child2 再次切回 child1 时,由于 child1 只是被停用而没有被销毁,所以不会再次调用 mounted 钩子函数,此时会执行 activateChildComponent 函数对组件的状态进行处理, activateChildComponent 和前面分析过的 deactivateChildComponent 方法类似,都是对组件的启用状态进行更新处理。

export function activateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = false
    if (isInInactiveTree(vm)) {
      return
    }
  } else if (vm._directInactive) {
    return
  }
  if (vm._inactive || vm._inactive === null) {
    // 标记组件处于启用状态
    vm._inactive = false
    for (let i = 0; i < vm.$children.length; i++) {
      // 递归处理子组件的启用状态。
      activateChildComponent(vm.$children[i])
    }
    callHook(vm, 'activated')
  }
}

缓存优化 - LRU 算法

程序的内存空间使用有限的,所以我们无法无节制的对数据进行储存,这需要有策略去淘汰不那么重要的数据,保持最大数据存储量的一致。

根据淘汰机制的不同,常用有以下三种:

    1. FIFO:先进先出策略 通过记录数据使用使用的使用,当缓存大小即将溢出时,优先清除里当前时间最远的数据
    1. LRU:最近最少使用 LRU 策略遵循的原则时, 如果数据最近被使用(访问)过,那么认为将来被访问的概率会更改。如果使用一个数组去记录数据,当一段数据被访问时,该数据会被移动到数组的末尾,表明该数据最近被使用过,当缓存溢出时,会删除数据头部的数据,即将最少使用的数据移除。

    对于 LRU 算法,个人认为翻译成最近最久未使用会贴切一点,在算法的实现过程中,并没有关注数据在指定时间段的使用次数,而是直接淘汰上一次使用时间距离当前时间最久的数据

    1. LFU: 计数最少策略 记录每一个数据的使用次数,当缓存溢出时,淘汰使用次数最少的数据。

上面三种缓存算法各有优劣,使用与不同的场景。而对于 Vuekeep-alive 在缓存组件时的优化处理,很明显利用了 LRU 的缓存策略,来看下关键代码

export default {
  // 渲染函数
  render () {
    if (cache[key]) {
      // 命中缓存,
      // make current key freshest
      remove(keys, key)
      keys.push(key)
    } else {
      // 初次渲染,缓存 vnode
      cache[key] = vnode
      keys.push(key)
      // prune oldest entry
      if (this.max && keys.length > parseInt(this.max)) {
        pruneCacheEntry(cache, keys[0], keys, this._vnode)
      }
    }
  }
}

export function remove (arr: Array<any>, item: any): Array<any> | void {
  if (arr.length) {
    const index = arr.indexOf(item)
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}

每次执行 render 函数时,会先查找缓存是否存在对应组件的缓存,如果不存在缓存,则添加缓存,并将组件的 key 保存在 keys 数据末尾,表明该组件时 keep-alive 最近一次渲染的子组件。如果查找到缓存,则将组件的 keykeys 数组中删除,重新添加到数据末尾,每次都执行相同的操作。

当缓存的数组大小超过规定的大小之后,删除 keys 数组头部的数据,并删除对应 key 的缓存数据,因为 keys 头部 key 对应的组件就是最久未使用的组件数据,符合 LRU 算法策略。

转载自:https://juejin.cn/post/7205162789155946554
评论
请登录