vue2和vue3 keep-alive的实现

883 阅读6分钟

keep-alive 是 Vue 提供的一个内置组件,用于缓存不活动的组件实例,而不是销毁它们。这样可以在组件切换时保留其状态或避免重新渲染。下面我们详细讲解 keep-alive 的原理,并对 Vue 2 和 Vue 3 的实现进行比较。

Vue 2 的 keep-alive 原理

在 Vue 2 中,keep-alive 组件通过包裹动态组件来实现缓存功能。其核心逻辑在于将不活动的组件实例缓存起来,当需要重新激活时,从缓存中取出,而不是重新创建实例。

Vue 2 实现代码(简化版)

以下是 Vue 2 中 keep-alive 的核心代码(简化版):

const KeepAlive = {
  name: 'keep-alive', // 定义组件的名字为 keep-alive
  abstract: true, // 将组件标记为抽象组件。抽象组件不会在父组件链中出现。

  props: {
    include: [String, RegExp, Array], // include:指定哪些组件需要被缓存,可以是字符串、正则表达式或数组。
    exclude: [String, RegExp, Array], // 指定哪些组件不需要被缓存,可以是字符串、正则表达式或数组
    max: [String, Number] // 缓存组件的最大数量
  },

  created() {
    this.cache = Object.create(null); // 用于存储缓存的组件实例。
    this.keys = []; // 用于存储缓存组件的键列表,按顺序保存。
  },

  destroyed() {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this._vnode); // 遍历 this.cache 中的所有键,并调用 pruneCacheEntry 函数清理缓存
    }
  },

  mounted() {
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name)); // 监听 include 属性的变化,并调用 pruneCache 函数清理不匹配的缓存。
    });
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name)); // 监听 exclude 属性的变化,并调用 pruneCache 函数清理匹配的缓存。
    });
  },

  render() { // 渲染函数,定义组件的渲染逻辑。
    const slot = this.$slots.default; // 获取默认插槽内容。
    const vnode = getFirstComponentChild(slot); // 获取插槽内容中的第一个子组件虚拟节点。
    const componentOptions = vnode && vnode.componentOptions; // 获取子组件的选项。

    if (componentOptions) { // 子组件的选项 存在时,继续处理。
      const name = getComponentName(componentOptions);
      const { include, exclude } = this;
      /**
       * 检查 include 和 exclude 属性。
       * 如果 include 存在且组件名称不匹配,则直接返回 vnode。
       * 如果 exclude 存在且组件名称匹配,则直接返回 vnode。
       */
      if (
        (include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        return vnode;
      }
      /**
       * 生成缓存键 key。
       * 如果 vnode.key 为空,则使用组件构造函数的 cid 和标签名生成键。
       * 否则,使用 vnode.key 作为键。
       */
      const key = vnode.key == null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key;
      /**
       * 检查缓存中是否存在该键。
       * 如果存在,从缓存中取出组件实例,并将键移到 this.keys 的末尾。
       * 如果不存在,将组件虚拟节点存入缓存,并将键添加到 this.keys。
       * 如果缓存数量超过 max,则清理最早的缓存。
       */
      if (this.cache[key]) {
        vnode.componentInstance = this.cache[key].componentInstance;
        remove(this.keys, key);
        this.keys.push(key);
      } else {
        this.cache[key] = vnode;
        this.keys.push(key);
        if (this.max && this.keys.length > parseInt(this.max)) {
          pruneCacheEntry(this.cache, this.keys[0], this._vnode);
        }
      }

      vnode.data.keepAlive = true; // 标记虚拟节点为 keepAlive
    }

    return vnode || (slot && slot[0]); // 返回虚拟节点或插槽中的第一个子节点
  }
};
/**
 * 清理缓存条目。
 * 获取缓存条目 entry。
 * 如果存在且不等于当前虚拟节点,销毁组件实例。
 * 将缓存条目置为空。
 */
function pruneCacheEntry(cache, key, current) {
  const entry = cache[key];
  if (entry && (!current || entry.tag !== current.tag)) {
    entry.componentInstance.$destroy();
  }
  cache[key] = null;
}
/**
 * 根据过滤条件清理缓存。
 * 获取 cache、keys 和 _vnode。
 * 遍历缓存,获取组件名称。
 * 如果名称存在且不符合过滤条件,清理缓存条目。
 */
function pruneCache(keepAliveInstance, filter) {
  const { cache, keys, _vnode } = keepAliveInstance;
  for (const key in cache) {
    const entry = cache[key];
    if (entry) {
      const name = getComponentName(entry.componentOptions);
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, _vnode);
      }
    }
  }
}
/**
 * 检查名称是否匹配模式。
 * 如果模式是数组,检查名称是否在数组中。
 * 如果模式是字符串,拆分字符串并检查名称是否在其中。
 * 如果模式是正则表达式,测试名称是否匹配。
 * 否则返回 false。
 */
function matches(pattern, name) {
  if (Array.isArray(pattern)) {
    return pattern.indexOf(name) > -1;
  } else if (typeof pattern === 'string') {
    return pattern.split(',').indexOf(name) > -1;
  } else if (pattern.test) {
    return pattern.test(name);
  }
  return false;
}
// 获取组件构造函数的 name 或标签名
function getComponentName(opts) {
  return opts && (opts.Ctor.options.name || opts.tag);
}
// 获取第一个子组件 过滤并返回第一个有 componentOptions 的子节点。
function getFirstComponentChild(children) {
  return children && children.filter(c => c && c.componentOptions)[0];
}

Vue 3 的 keep-alive 原理

在 Vue 3 中,keep-alive 组件的实现有所变化,主要是因为 Vue 3 使用了新的虚拟 DOM 和响应式系统。不过,核心思想仍然是缓存组件实例。

Vue 3 实现代码(简化版)

以下是 Vue 3 中 keep-alive 的核心代码(简化版):

import { defineComponent, onMounted, onUnmounted, ref, watch, getCurrentInstance } from 'vue';

const KeepAlive = defineComponent({
  name: 'KeepAlive',
  props: {
    include: [String, RegExp, Array], // 指定需要缓存的组件,可以是字符串、正则表达式或数组。
    exclude: [String, RegExp, Array], // 指定不需要缓存的组件,可以是字符串、正则表达式或数组。
    max: [String, Number] // 缓存组件的最大数量
  },
  setup(props, { slots }) {
    const cache = new Map(); // 用 Map 存储缓存的组件实例
    const keys = new Set(); // 用 Set 存储缓存组件的键
    let current = null; // 当前激活的组件实例

    // 返回组件选项的 name 或 __name
    function getComponentName(opts) {
        return opts && (opts.name || opts.__name);
    }
    // 查找并返回第一个有 type 且 type.__isKeepAlive 为 true 的子节点
    function getFirstComponentChild(children) {
        return children && children.find(c => c.type && c.type.__isKeepAlive);
    }

    const instance = getCurrentInstance(); // 获取当前组件实例
    // 定义激活函数
    instance.ctx.activate = (vnode, container, anchor) => {
      // move 函数通常定义在渲染器的实现中 用于将虚拟节点(VNode)移动到指定的 DOM 容器中
      move(vnode, container, anchor); // 将虚拟节点移动到容器中
      /**
       * 在渲染后执行的队列中添加函数。
       * 设置组件为激活状态。
       * 如果组件有激活钩子函数(a),则调用它们。
       */
      queuePostRenderEffect(() => {
        vnode.component.isDeactivated = false;
        if (vnode.component.a) {
          invokeArrayFns(vnode.component.a);
        }
      }, parentSuspense);
    };
    // 定义停用函数
    instance.ctx.deactivate = (vnode) => {
      move(vnode, storageContainer, null); // 将虚拟节点移动到存储容器中
      /**
       * 在渲染后执行的队列中添加函数。
       * 设置组件为停用状态。
       * 如果组件有停用钩子函数(da),则调用它们。
       */
      queuePostRenderEffect(() => {
        vnode.component.isDeactivated = true;
        if (vnode.component.da) {
          invokeArrayFns(vnode.component.da);
        }
      }, parentSuspense);
    };

    /**
     * 清理缓存条目
     * 获取缓存的组件实例。
     * 如果存在,设置组件为停用状态。
     * 如果组件有停用钩子函数(da),则调用它们。
     * 从缓存和键集合中删除该条目。
     */
    function pruneCacheEntry(key) {
      const cached = cache.get(key);
      if (cached) {
        cached.component.isDeactivated = true;
        if (cached.component.da) {
          invokeArrayFns(cached.component.da);
        }
        cache.delete(key);
        keys.delete(key);
      }
    }
    /**
     * 监听 include 和 exclude 属性的变化。
     * 遍历缓存,获取组件名称。
     * 如果名称不符合 include 或符合 exclude,则清理缓存条目。
     */
    watch(
      () => [props.include, props.exclude],
      ([include, exclude]) => {
        cache.forEach((vnode, key) => {
          const name = getComponentName(vnode.type);
          if (
            (include && (!name || !matches(include, name))) ||
            (exclude && name && matches(exclude, name))
          ) {
            pruneCacheEntry(key);
          }
        });
      },
      { flush: 'post', deep: true }
    );
    return () => {
      const children = slots.default(); // 获取默认插槽内容。
      const vnode = getFirstComponentChild(children) // 获取插槽内容中的第一个子组件虚拟节点;
      const componentOptions = vnode && vnode.type // 获取子组件的选项;
      if (componentOptions) {
        const name = getComponentName(componentOptions); // 获取子组件的名称
        const { include, exclude } = props; // 解构 include 和 exclude 属性
        /**
         * 检查 include 和 exclude 属性。
         * 如果 include 存在且组件名称不匹配,则直接返回 vnode。
         * 如果 exclude 存在且组件名称匹配,则直接返回 vnode
         */
        if (
          (include && (!name || !matches(include, name))) ||
          (exclude && name && matches(exclude, name))
        ) {
          return vnode;
        }
        /**
         * 生成缓存键 key
         * 如果 vnode.key 为空,则使用组件选项作为键 否则,使用 vnode.key 作为键
         */
        const key = vnode.key == null ? componentOptions : vnode.key;
        /**
         * 检查缓存中是否存在该键。
         * 如果存在,从缓存中取出组件实例,并将键移到 keys 的末尾。
         * 如果不存在,将组件虚拟节点存入缓存,并将键添加到 keys。
         * 如果缓存数量超过 max,则清理最早的缓存。
         * 标记虚拟节点为 keepAlive。
         * 返回虚拟节点。
         */
        if (cache.has(key)) {
          vnode.component = cache.get(key).component;
          keys.delete(key);
          keys.add(key);
        } else {
          cache.set(key, vnode);
          keys.add(key);
          if (props.max && keys.size > parseInt(props.max, 10)) {
            pruneCacheEntry(Array.from(keys)[0]);
          }
        }
        vnode.shapeFlag |= 256;
      }
      return vnode;
    };

  }
})

export default KeepAlive;
  1. 缓存机制

    • Vue 2 使用了对象字面量和数组来管理缓存。
    • Vue 3 使用了 Mapref 来管理缓存和键列表。
  2. 代码结构

    • Vue 2 的 keep-alive 组件是使用对象语法定义的。
    • Vue 3 则使用了 defineComponent 和组合式 API。
使用文档参考 KeepAlive | Vue.js (vuejs.org)