<keep-alive> 缓存及其缓存优化原理

更新日期: 2020-02-03阅读: 1.8k标签: 缓存

缓存淘汰策略

由于 <keep-alive> 中的缓存优化遵循 LRU 原则,所以首先了解下缓存淘汰策略的相关介绍。

由于缓存空间是有限的,所以不能无限制的进行数据存储,当存储容量达到一个阀值时,就会造成内存溢出,因此在进行数据缓存时,就要根据情况对缓存进行优化,清除一些可能不会再用到的数据。所以根据缓存淘汰的机制不同,常用的有以下三种:

FIFO(fisrt-in-fisrt-out)- 先进先出策略

我们通过记录数据使用的时间,当缓存大小即将溢出时,优先清楚离当前时间最远的数据。


LRU (least-recently-used)- 最近最少使用策略

以时间作为参考,如果数据最近被访问过,那么将来被访问的几率会更高,如果以一个数组去记录数据,当有一数据被访问时,该数据会被移动到数组的末尾,表明最近被使用过,当缓存溢出时,会删除数组的头部数据,即将最不频繁使用的数据移除。(keep-alive 的优化处理)


LFU (least-frequently-used)- 计数最少策略

以次数作为参考,用次数去标记数据使用频率,次数最少的会在缓存溢出时被淘汰。



<keep-alive> 简单示例

首先我们看一个动态组件使用 <keep-alive> 的例子)。

<div id="dynamic-component-demo">
  <button v-on:click="currentTab = 'Posts'">Posts</button>
    <button v-on:click="currentTab = 'Archive'">Archive</button>
  <keep-alive>
    <component
      v-bind:is="currentTabComponent"
     
    ></component>
  </keep-alive>
</div>
vue.component('tab-posts', { 
  data: function () {
      return {
      count: 0
    }
  },
    template: `
      <div>
     <button @click="count++">Click Me</button>
         <p>{{count}}</p>
    </div>`
})

Vue.component('tab-archive', { 
    template: '<div>Archive component</div>' 
})

new Vue({
  el: '#dynamic-component-demo',
  data: {
    currentTab: 'Posts',
  },
  computed: {
    currentTabComponent: function () {
      return 'tab-' + this.currentTab.toLowerCase()
    }
  }
})

我们可以看到,动态组件外层包裹着 <keep-alve> 标签。

<keep-alive>
  <component
    v-bind:is="currentTabComponent"
    class="tab"
  ></component>
</keep-alive>

那就意味着,当选项卡 Posts 、 Archive 在来回切换时,所对应的组件实例会被缓存起来,所以当再次切换到 Posts 选项时,其对应的组件 tab-posts 会从缓存中获取,计数器 count 也会保留上一次的状态。


<keep-alive> 缓存及优化处理

就此,我们看完 <keep-alive> 的简单示例之后,让我们一起来分析下源码中它是如何进行组件缓存和缓存优化处理的。

首次渲染

vue 在模板 -> AST -> render() -> vnode -> 真实dom 这个转化过程中,会进入 patch 阶段,在patch 阶段,会调用 createElm 函数中会将 vnode 转化为真实 dom 。

function createPatchFunction (backend) {
  ...
  //生成真实dom
  function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    // 返回 true 代表为 vnode 为组件 vnode,将停止接下来的转换过程
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return;
    }
    ...
  }
}

在转化节点的过程中,因为 <keep-alive> 的 vnode 会视为组件 vnode,因此一开始会调用 createComponent() 函数,createComponent() 会执行组件初始化内部钩子 init(), 对组件进行初始化和实例化。

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    var i = vnode.data;
    if (isDef(i)) {
      // isreactivated 用来判断组件是否缓存
      var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        // 执行组件初始化的内部钩子 init()
        i(vnode, false /* hydrating */);
      }
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue);
        // 将真实 dom 添加到父节点,insert 操作 dom api
        insert(parentElm, vnode.elm, refElm);
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
        }
        return true
      }
    }
  }

<keep-alive> 组件通过调用内部钩子 init() 方法进行初始化操作。

注:源码中通过函数 installComponentHooks() 可追踪到内部钩子的定义对象 componentVNodeHooks。
// inline hooks to be invoked on component VNodes during patch
var componentVNodeHooks = {
  init: function init (vnode, hydrating) {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      var mountedNode = vnode; // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    } else {
      // 第一次运行时,vnode.componentInstance 不存在 ,vnode.data.keepAlive 不存在
      // 将组件实例化,并赋值给 vnode 的 componentInstance 属性
      var child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      );
      // 进行挂载
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
  },
  // prepatch 是 patch 过程的核心步骤
  prepatch: function prepatch (oldVnode, vnode) { ... },
  insert: function insert (vnode) { ... },
  destroy: function destroy (vnode) { ... }
};

第一次执行时,很明显组件 vnode 没有 componentInstance 属性,vnode.data.keepAlive 也没有值,所以会调用 createComponentInstanceForVnode() 将组件进行实例化并将组件实例赋值给 vnode 的componentInstance 属性,最后执行组件实例的 $mount 方法进行实例挂载。

createComponentInstanceForVnode()是组件实例化的过程,组件实例化无非就是一系列选项合并,初始化事件,生命周期等初始化操作。

缓存 vnode 节点

<keep-alive> 在执行组件实例化之后会进行组件的挂载(如上代码所示)。

...
// 进行挂载
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
...

挂载 $mount 阶段会调用 mountComponent() 函数进行 vm._update(vm._render(), hydrating); 操作。

Vue.prototype.$mount = function (el, hydrating) {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating)
};

function mountComponent (vm, el, hydrating) {
  vm.$el = el;
    ...
  callHook(vm, 'beforeMount');
  var updateComponent;
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    ...
  } else { 
    updateComponent = function () {
      // vm._render() 会根据数据的变化为组件生成新的 Vnode 节点
      // vm._update() 最终会为 Vnode 生成真实 DOM 节点
      vm._update(vm._render(), hydrating);
    }
  }
  ...
  return vm
}

而 vm._render() 函数最终会调用组件选项中的 render() 函数,进行渲染。

function renderMixin (Vue) {
  ...
  Vue.prototype._render = function () {
    var vm = this;
    var ref = vm.$options;
    var render = ref.render;
    ...
    try {  
      ...
      // 调用组件的 render 函数
      vnode = render.call(vm._renderProxy, vm.$createElement);
    }
    ...
    return vnode
  };
}

由于keep-alive 是一个内置组件,因此也拥有自己的 render() 函数,所以让我们一起来看下 render() 函数的具体实现。

var KeepAlive = {
  ...
  props: {
    include: patternTypes,  // 名称匹配的组件会被缓存,对外暴露 include 属性 api
    exclude: patternTypes,  // 名称匹配的组件不会被缓存,对外暴露 exclude 属性 api
    max: [String, Number]  // 可以缓存的组件最大个数,对外暴露 max 属性 api
  },
  created: function created () {},
  destroyed: function destroyed () {},
    mounted: function mounted () {},
  
  // 在渲染阶段,进行缓存的存或者取
  render: function render () {
    // 首先拿到 keep-alve 下插槽的默认值 (包裹的组件)
    var slot = this.$slots.default;
    // 获取第一个 vnode 节点
    var vnode = getFirstComponentChild(slot); // # 3802 line
    // 拿到第一个子组件实例
    var componentOptions = vnode && vnode.componentOptions;
    // 如果 keep-alive 第一个组件实例不存在
    if (componentOptions) {
      // check pattern
      var name = getComponentName(componentOptions);
      var ref = this;
      var include = ref.include;
      var exclude = ref.exclude;
      // 根据匹配规则返回 vnode 
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      var ref$1 = this;
      var cache = ref$1.cache;
      var keys = ref$1.keys;
      var key = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        // 获取本地组件唯一key
        ? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
        : vnode.key;
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance;
        // make current key freshest
        // 使用 LRU 最近最少缓存策略,将命中的 key 从缓存数组中删除,并将当前最新 key 存入缓存数组的末尾
        remove(keys, key); // 删除命中已存在的组件
        keys.push(key); // 将当前组件名重新存入数组最末端
      } else {
        // 进行缓存
        cache[key] = vnode;
        keys.push(key);
        // prune oldest entry
        // 根据组件名与 max 进行比较
        if (this.max && keys.length > parseInt(this.max)) { // 超出组件缓存最大数的限制
          // 执行 pruneCacheEntry 对最少访问数据(数组的第一项)进行删除
          pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }
      // 为缓存组件打上标志
      vnode.data.keepAlive = true;
    }
    // 返回 vnode 
    return vnode || (slot && slot[0])
  }
};

从上可得知,在 keep-alive 的源码定义中, render() 阶段会缓存 vnode 和组件名称 key 等操作。

首先会判断是否存在缓存,如果存在,则直接从缓存中获取组件的实例,并进行缓存优化处理(稍后会介绍到)。

如果不存在缓存,会将 vnode 作为值存入 cache 对象对应的 key 中。还会将组件名称存入 keys 数组中。

if (cache[key]) {
  vnode.componentInstance = cache[key].componentInstance;
  // make current key freshest
  // 使用 LRU 最近最少缓存策略,将命中的 key 从缓存数组中删除,并将当前最新 key 存入缓存数组的末尾
  remove(keys, key); // 删除命中已存在的组件
  keys.push(key); // 将当前组件名重新存入数组最末端
} else {
  // 进行缓存
  cache[key] = vnode;
  keys.push(key);
  // prune oldest entry
  // 根据组件名与 max 进行比较
  if (this.max && keys.length > parseInt(this.max)) { // 超出组件缓存最大数的限制
    // 执行 pruneCacheEntry 对最少访问数据(数组的第一项)进行删除
    pruneCacheEntry(cache, keys[0], keys, this._vnode);
  }
}

缓存真实 DOM

回顾之前提到的首次渲染阶段,会调用 createComponent()函数, createComponent()会执行组件初始化内部钩子 init(),对组件进行初始化和实例化等操作。

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  var i = vnode.data;
  if (isDef(i)) {
    // isReactivated 用来判断组件是否缓存
    var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
    if (isDef(i = i.hook) && isDef(i = i.init)) {
        // 执行组件初始化的内部钩子 init()
      i(vnode, false /* hydrating */);
    }
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue);
      // 将真实 dom 添加到父节点,insert 操作 dom api
      insert(parentElm, vnode.elm, refElm);
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
      }
      return true
    }
  }
}

createComponet() 函数还会我们通过 vnode.componentInstance 拿到了 <keep-alive> 组件的实例,然后执行 initComponent() ,initComponent() 函数的作用就是将真实的 dom 保存再 vnode 中。

...
if (isDef(vnode.componentInstance)) {
  // 其中的一个作用就是保存真实 dom 到 vnode 中
  initComponent(vnode, insertedVnodeQueue);
  // 将真实 dom 添加到父节点,(insert 操作 dom api)
  insert(parentElm, vnode.elm, refElm);
  if (isTrue(isReactivated)) {
      reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
  }
  return true
}
...
function initComponent (vnode, insertedVnodeQueue) {
    if (isDef(vnode.data.pendingInsert)) {
      insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
      vnode.data.pendingInsert = null;
    }
    // 保存真是 dom 节点到 vnode 
    vnode.elm = vnode.componentInstance.$el;
    ...
}

缓存优化处理

文章开头,我们介绍了三种缓存优化策略(它们各有优劣),而在 vue 中对 <keep-alive> 的缓存优化处理的实现上,便用到了上述的 LRU 缓存策略 。

上面介绍到,<keep-alive> 组件在存取缓存的过程中,是在渲染阶段进行的,所以我们回过头来看 render() 函数的实现。

var KeepAlive = {
  ...
  props: {
    include: patternTypes,  // 名称匹配的组件会被缓存,对外暴露 include 属性 api
    exclude: patternTypes,  // 名称匹配的组件不会被缓存,对外暴露 exclude 属性 api
    max: [String, Number]  // 可以缓存的组件最大个数,对外暴露 max 属性 api
  },
  // 创建节点生成缓存对象
  created: function created () {
    this.cache = Object.create(null); // 缓存 vnode 
    this.keys = []; // 缓存组件名
  },
 
  // 在渲染阶段,进行缓存的存或者取
  render: function render () {
    // 首先拿到 keep-alve 下插槽的默认值 (包裹的组件)
    var slot = this.$slots.default;
    // 获取第一个 vnode 节点
    var vnode = getFirstComponentChild(slot); // # 3802 line
    // 拿到第一个子组件实例
    var componentOptions = vnode && vnode.componentOptions;
    // 如果 keep-alive 第一个组件实例不存在
    if (componentOptions) {
      // check pattern
      var name = getComponentName(componentOptions);
      var ref = this;
      var include = ref.include;
      var exclude = ref.exclude;
      // 根据匹配规则返回 vnode 
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      var ref$1 = this;
      var cache = ref$1.cache;
      var keys = ref$1.keys;
      var key = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        // 获取本地组件唯一key
        ? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
        : vnode.key;
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance;
        // make current key freshest
        // 使用 LRU 最近最少缓存策略,将命中的 key 从缓存数组中删除,并将当前最新 key 存入缓存数组的末尾
        remove(keys, key); // 删除命中已存在的组件
        keys.push(key); // 将当前组件名重新存入数组最末端
      } else {
        // 进行缓存
        cache[key] = vnode;
        keys.push(key);
        // prune oldest entry
        // 根据组件名与 max 进行比较
        if (this.max && keys.length > parseInt(this.max)) { // 超出组件缓存最大数的限制
          // 执行 pruneCacheEntry 对最少访问数据(数组的第一项)进行删除
          pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }
      // 为缓存组件打上标志
      vnode.data.keepAlive = true;
    }
    // 返回 vnode 
    return vnode || (slot && slot[0])
  }
};

<keep-alive> 组件会在创建阶段生成缓存对象,在渲染阶段对组件进行缓存,并进行缓存优化。我们重点来看下段带代码。

if (cache[key]) {
  ...
  // 使用 LRU 最近最少缓存策略,将命中的 key 从缓存数组中删除,并将当前最新 key 存入缓存数组的末尾
  remove(keys, key); // 删除命中已存在的组件
  keys.push(key); // 将当前组件名重新存入数组最末端
} else {
  // 进行缓存
  cache[key] = vnode;
  keys.push(key);
  // 根据组件名与 max 进行比较
  if (this.max && keys.length > parseInt(this.max)) { // 超出组件缓存最大数的限制
    // 执行 pruneCacheEntry 对最少访问数据(数组的第一项)进行删除
    pruneCacheEntry(cache, keys[0], keys, this._vnode);
  }
}

从注释中我们可以得知,当 keep-alive 被激活时(触发 activated 钩子),会执行 remove(keys, key) 函数,从缓存数组中 keys 删除已存在的组件,之后会进行 push 操作,将当前组件名重新存入 keys 数组的最末端,正好符合 LRU 。

LRU:以时间作为参考,如果数据最近被访问过,那么将来被访问的几率会更高,如果以一个数组去记录数据,当有一数据被访问时,该数据会被移动到数组的末尾,表明最近被使用过,当缓存溢出时,会删除数组的头部数据,即将最不频繁使用的数据移除。
remove(keys, key); // 删除命中已存在的组件
keys.push(key); // 将当前组件名重新存入数组最末端

function remove (arr, item) {
  if (arr.length) {
    var index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}

至此,我们可以回过头看我们上边的 <keep-alive> 示例,示例中包含 tab-posts、tab-archive 两个组件,通过 component 的 is 属性动态渲染。当 tab 来回切换时,会将两个组件的 vnode 和组件名称存入缓存中,如下。

keys = ['tab-posts', 'tab-archive']
cache = {
    'tab-posts':   tabPostsVnode,
    'tab-archive': tabArchiveVnode
}

假如,当再次激活到 tabPosts 组件时,由于命中了缓存,会调用源码中的 remove()方法,从缓存数组中 keys 把 tab-posts 删除,之后会使用 push 方法将 tab-posts 推到末尾。这时缓存结果变为:

keys = ['tab-archive', 'tab-posts']
cache = {
    'tab-posts':   tabPostsVnode,
    'tab-archive': tabArchiveVnode
}

现在我们可以得知,keys 用开缓存组件名是用来记录缓存数据的。 那么当缓存溢出时, <keep-alive>又是如何 处理的呢?

我们可以通过 max 属性来限制最多可以缓存多少组件实例。

在上面源码中的 render() 阶段,还有一个 pruneCacheEntry(cache, keys[0], keys, this._vnode) 函数,根据 LRU 淘汰策略,会在缓存溢出时,删除缓存中的头部数据,所以会将 keys[0] 传入pruneCacheEntry() 。

if (this.max && keys.length > parseInt(this.max)) { // 超出组件缓存最大数的限制
  // 执行 pruneCacheEntry 对最少访问数据(数组的第一项)进行删除
  pruneCacheEntry(cache, keys[0], keys, this._vnode);
}

pruneCacheEntry() 具体逻辑如下:

首先,通过cached$$1 = cache[key]` 获取头部数据对应的值 `vnode`,执行 `cached$$1.componentInstance.$destroy() 将组件实例销毁。

其次,执行 cache[key] = null 清空组件对应的缓存节点。

最后,执行 remove(keys, key) 删除缓存中的头部数据 keys[0]。

至此,关于 <keep-alive> 组件的首次渲染、组件缓存和缓存优化处理相关的实现就到这里。


最后

最后记住这几个点:

<keep-alive> 是 vue 内置组件,在源码定义中,也具有自己的组件选项如 data 、 method 、 computed 、 props 等。

<keep-alive> 具有抽象组件标识 abstract,通常会与动态组件一同使用。

<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,将它们停用,而不是销毁它们。

被 <keep-alive> 缓存的组件会触发 activated 或 deactivated 生命周期钩子。

<keep-alive> 会缓存组件实例的 vnode 对象 ,和真实 dom 节点,所以会有 max 属性设置。

<keep-alive> 不会在函数式组件中正常工作,因为它们没有缓存实例。

来自:https://segmentfault.com/a/1190000022252873

链接: https://fly63.com/article/detial/8491

浏览器缓存_HTTP强缓存和协商缓存

浏览器缓存主要分为强强缓存(也称本地缓存)和协商缓存(也称弱缓存),强缓存是利用http头中的Expires和Cache-Control两个字段来控制的,用来表示资源的缓存时间。协商缓存就是由服务器来确定缓存资源是否可用.

angularjs 缓存详解

一个缓存就是一个组件,它可以透明地存储数据,以便未来可以更快地服务于请求。缓存能够服务的请求越多,整体系统性能就提升得越多。

浏览器缓存问题原理以及解决方案

浏览器缓存就是把一个已经请求过的Web资源(如html页面,图片,js,数据等)拷贝一份副本储存在浏览器中,为什么使用缓存:减少网络带宽消耗,降低服务器压力,减少网络延迟,加快页面打开速度

Web缓存相关知识整理

一个H5页面在APP端,如果勾选已读状态,则下次打开该链接,会跳过此页面。用到了HTML5 的本地存储 API 中的 localStorage作为解决方案,回顾了下Web缓存的知识

使用缓存加速之后的网站访问过程变化

在描述CDN的实现原理之前,让我们先看传统的未加缓存服务的访问过程,以便了解CDN缓存访问方式与未加缓存访问方式的差别,用户访问未使用CDN缓存网站的过程为:用户向浏览器提供要访问的域名;

html页面清除缓存

页面打开时,由于缓存的存在,刚刚更新的数据有时无法在页面得到刷新,当这个页面作为模式窗口被打开时问题更为明显, 如何将缓存清掉?

HTTP之缓存 Cache-Control

通过在Response Header设置Cache-Control head 信息可以控制浏览器的缓存行为。我们先来看一下Cache-Control可以设置哪些值:缓存头Cache-Control只能在服务端设置,在客户端是由浏览器设置的,自己不能修改它的值。

工程化_前端静态资源缓存策略

增量更新是目前大部分团队采用的缓存更新方案,能让用户在无感知的情况获取最新内容。具体实现方式通常是(一般我们通过构建工具来实现,比如webpack):

前端静态资源自动化处理版本号防缓存

浏览器会默认缓存网站的静态资源文件,如:js文件、css文件、图片等。缓存带来网站性能提升的同时也带来了一些困扰,最常见的问题就是不能及时更新静态资源,造成新版本发布时用户无法及时看到新版本的变化,严重影响了用户体验。

vue后台管理系统解决keep-alive页面路由参数变化时缓存问题

一个后台管理系统,一个列表页A路由配置需要缓存,另一个页面B里面有多个跳转到A路由的链接。问题描述:首先访问/A?id=1页面,然后到B页面再点击访问A?id=2的页面,发现由于页面A设置了缓存,数据还是id=1的数据,并没有更新。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!