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,
    };
  }

  // 省略部分代码
}
相关推荐
一颗花生米。2 小时前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
学习使我快乐012 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19952 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
勿语&3 小时前
Element-UI Plus 暗黑主题切换及自定义主题色
开发语言·javascript·ui
黄尚圈圈3 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水4 小时前
简洁之道 - React Hook Form
前端
正小安6 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch8 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光8 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   8 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发