Vue3 KeepAlive组件原理

这个组件与渲染器联系很深并且要知道渲染器是怎么渲染普通组件,关于渲染器可以看这篇渲染组件看这篇

KeepAlive 的本质是缓存管理,再加上特殊的挂载/卸载逻辑

Vue的 KeepAlive 组件可以避免被它包裹的组件被频繁地 销毁/重建。 例如

js 复制代码
<KeepAlive>
    <Tab v-if="currentTab === 1">...</Tab>
    <Tab v-if="currentTab === 2">...</Tab>
    <Tab v-if="currentTab === 3">...</Tab>
</KeepAlive>

如果 Tab组件没有被KeepAlive包裹,v-if将真实的卸载DOM,挂载DOM。我们都知道这是很消耗性能的。

而且我们可能会遇到一种情况:如果在Tab组件填写了一个表单,然后切换tab,再切换回原来的Tab,可以看到填写的表单数据都消失了。这就是因为组件被卸载在挂载,组件的状态已经被重置了。

那么 KeepAlive做了什么? 就是将被 KeepAlive 的组件从原容器搬运到另外一个隐藏的容器中,实现"假卸载"。当被搬运到隐藏容器中的组件需要再次被"挂载"时,我们也不能执行真正的挂载逻辑,而应该把该组件从隐藏容器中再搬运到原容器。这个过程对应到组件的生命周期,其实就是 activateddeactivated

因此 KeepAlive没有进行卸载操作,只是隐藏了组件,所以组件的状态得以保存,也就实现了缓存

接下来实现一个KeepAlive组件

  1. 与普通组件相同都是一个对象,但是给它一个独有的属性__isKeepAlive,用作标识。
  2. 并且也有setup函数,首先在setup函数中创建一个缓存对象,再获取当前 KeepAlive 组件的实例instance也就是currentInstance(currentInstance是一个全局变量,在mountComponent中会先创建一个要挂载组件对应的instance,再把它赋给currentInstance)。
  3. 因为要把keepalive的内部组件放到一个隐藏容器,之后再把它拿出来,这就需要渲染器创建新元素,移动元素的能力,所以要把渲染器的方法也暴露给组件
  4. 接着在实例上会被添加两个内部函数,分别是 _deActivate 和 _activate。它们就使用了渲染器的方法对内部组件进行移动。
  5. 我们之前说过setup也可以返回一个函数作为渲染函数,这里就是返回一个函数。KeepAlive中的内部组件即是它的默认插槽。
  6. 第一次挂载将内部组件添加到缓存对象cache.set(rawVNode.type, rawVNode),如果在缓存对象中能找到,代表应该执行激活,继承组件实例,并打上keepalive的标记。避免重新挂载
  7. 将 KeepAlive 组件的实例也添加到 vnode 上,以便在渲染器中访问,在组件 vnode 上添加 shouldKeepAlive 属性,并标记为 true,避免渲染器真的将组件卸载
js 复制代码
const KeepAlive = {
  // KeepAlive 组件独有的属性,用作标识
  __isKeepAlive: true,
  setup(props, { slots }) {
    // 创建一个缓存对象
    // key: vnode.type
    // value: vnode
    const cache = new Map();
    // 当前 KeepAlive 组件的实例
    const instance = currentInstance;
    // 对于 KeepAlive 组件来说,它的实例上存在特殊的 keepAliveCtx 对象,该对象由渲染器注入
    // 该对象会暴露渲染器的一些内部方法,其中 move 函数用来将一段 DOM 移动到另一个容器中
    const { move, createElement } = instance.keepAliveCtx;

    // 创建隐藏容器
    const storageContainer = createElement("div");

    // KeepAlive 组件的实例上会被添加两个内部函数,分别是 _deActivate和 _activate
    // 这两个函数会在渲染器中被调用
    instance._deActivate = (vnode) => {
      move(vnode, storageContainer);
    };
    instance._activate = (vnode, container, anchor) => {
      move(vnode, container, anchor);
    };

    return () => {
      // KeepAlive 的默认插槽就是要被 KeepAlive 的组件
      let rawVNode = slots.default();
      // 如果不是组件,直接渲染即可,因为非组件的虚拟节点无法被 KeepAlive
      if (typeof rawVNode.type !== "object") {
        return rawVNode;
      }

      // 在挂载时先获取缓存的组件 vnode
      const cachedVNode = cache.get(rawVNode.type);
      if (cachedVNode) {
        // 如果有缓存的内容,则说明不应该执行挂载,而应该执行激活
        // 继承组件实例
        rawVNode.component = cachedVNode.component;
        // 在 vnode 上添加 keptAlive 属性,标记为 true,避免渲染器重新挂载它
        rawVNode.keptAlive = true;
      } else {
        // 如果没有缓存,则将其添加到缓存中,这样下次激活组件时就不会执行新的挂载动作了
        cache.set(rawVNode.type, rawVNode);
      }

      // 在组件 vnode 上添加 shouldKeepAlive 属性,并标记为 true,避免渲染器真的将组件卸载
      rawVNode.shouldKeepAlive = true;
      // 将 KeepAlive 组件的实例也添加到 vnode 上,以便在渲染器中访问
      rawVNode.keepAliveInstance = instance;

      // 渲染组件 vnode
      return rawVNode;
    };
  },
};

接下来考虑内部组件触发unmount,判断是否是组件并且该组件shouldKeepAlive为true,代表这个组件不应该被卸载,而是触发_deActivate。在上面已经把keepalive实例加在了内部组件VNode,所以可以直接调用_deActivate

js 复制代码
else if (typeof vnode.type === 'object') {
   // vnode.shouldKeepAlive 是一个布尔值,用来标识该组件是否应该被 KeepAlive
   if (vnode.shouldKeepAlive) {
   // 对于需要被 KeepAlive 的组件,我们不应该真的卸载它,而应调用该组件的父组件,
   // 即 KeepAlive 组件的 _deActivate 函数使其失活
   vnode.keepAliveInstance._deActivate(vnode)
   } else {
   unmount(vnode.component.subTree)
   }
   return
   }

对应内部组件的挂载,第一次执行setup函数,给缓存对象里加上了内部组件,之后触发setup注册的副作用函数,就可以在缓存对象里找到这个内部组件并他的keptAlive属性设为true.

这样再次执行patch就可以根据这个属性判断要不要挂载。

js 复制代码
else if (typeof type === 'object' || typeof type ===
'function') {
 // component
 if (!n1) {
 // 如果该组件已经被 KeepAlive,则不会重新挂载它,而是会调用_activate 来激活它
 if (n2.keptAlive) {
 n2.keepAliveInstance._activate(n2, container,anchor)
 } else {
 mountComponent(n2, container, anchor)
 }
 } else {
 patchComponent(n1, n2, anchor)
 }
 }

最后在mountComponent将渲染器的方法传给keepalive的实例

js 复制代码
function mountComponent(vnode, container, anchor) {
  // 省略部分代码

  const instance = {
    state,
    props: shallowReactive(props),
    isMounted: false,
    subTree: null,
    slots,
    mounted: [],
    // 只有 KeepAlive 组件的实例下会有 keepAliveCtx 属性
    keepAliveCtx: null,
  };

  // 检查当前要挂载的组件是否是 KeepAlive 组件
  const isKeepAlive = vnode.type.__isKeepAlive;
  if (isKeepAlive) {
    // 在 KeepAlive 组件实例上添加 keepAliveCtx 对象
    instance.keepAliveCtx = {
      // move 函数用来移动一段 vnode
      move(vnode, container, anchor) {
        // 本质上是将组件渲染的内容移动到指定容器中,即隐藏容器中
        insert(vnode.component.subTree.el, container, anchor);
      },
      createElement,
    };
  }

  // 省略部分代码
}
相关推荐
晴空万里藏片云36 分钟前
elment Table多级表头固定列后,合计行错位显示问题解决
前端·javascript·vue.js
曦月合一36 分钟前
html中iframe标签 隐藏滚动条
前端·html·iframe
奶球不是球38 分钟前
el-button按钮的loading状态设置
前端·javascript
kidding72342 分钟前
前端VUE3的面试题
前端·typescript·compositionapi·fragment·teleport·suspense
无责任此方_修行中2 小时前
每周见闻分享:杂谈AI取代程序员
javascript·资讯
Σίσυφος19003 小时前
halcon 条形码、二维码识别、opencv识别
前端·数据库
学代码的小前端3 小时前
0基础学前端-----CSS DAY13
前端·css
dorabighead4 小时前
JavaScript 高级程序设计 读书笔记(第三章)
开发语言·javascript·ecmascript
css趣多多4 小时前
案例自定义tabBar
前端
姑苏洛言5 小时前
DeepSeek写微信转盘小程序需求文档,这不比产品经理强?
前端