likes
comments
collection
share

Vue KeepAlive 为什么不能缓存 iframe

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

最近做了个项目,其中有个页面是由 iframe 嵌套了一个另外的页面,在运行的过程中发现 KeepAlive 并不生效,每次切换路由都会触发 iframe 页面的重新渲染,代码如下:

  <router-view v-slot="{ Component }">
    <keep-alive :include="keepAliveList">
      <component :is="Component"></component>
    </keep-alive>
  </router-view>

看起来并没有什么问题,并且其他非 iframe 实现的页面都是可以被缓存的,因此可以推断问题出在 iframe 的实现上。

我们先了解下 KeepAlive

KeepAlive (熟悉的可跳过本节)

被 KeepAlive 包裹的组件不是真的卸载,而是从原来的容器搬运到另外一个隐藏容器中,实现“假卸载”, 当被搬运的容器需要再次挂载时,应该把组件从隐藏容器再搬运到原容器,这个过程对应到组件的生命周期就是 activated 和 deactivated

keepAlive 是需要渲染器支持的,在执行 mountComponent 时,如果发现是 __isKeepAlive 组件,那么会在上下文注入 move 方法。

function mountComponent(vnode, container, anchor) {
  /**... */
  const instance = {
    /** ... */
    state,
    props: shallowReactive(props),
    // KeepAlive 实例独有
    keepAliveCtx: null
  };

  const isKeepAlive = vnode.__isKeepAlive;
  if (isKeepAlive) {
    instance.keepAliveCtx = {
      move(vnode, container, anchor) {
        insert(vnode.component.subTree.el, container, anchor);
      },
      createElement
    };
  }
}

实现一个最基本的 KeepAlive,需要注意几个点

  1. KeepAlive 组件会创建一个隐藏的容器 storageContainer
  2. KeepAlive 组件的实例增加两个方法 _deActive_active
  3. KeepAlive 组件存在一个缓存的 Map,并且缓存的值是 vnode
const KeepAlive = {
  // KeepAlive 特有的属性,用来标识
  __isKeepAlive: true,
  setup() {
    /**
     * 创建一个缓存对象
     * key: vnode.type
     * value: vnode
     */
    const cache = new Map();
    // 当前 keepAlive 组件的实例
    const instance = currentInstance;
    const { move, createElement } = instance.keepAliveCtx;
    // 创建隐藏容器
    const storageContainer = createElement('div');

    // 为 KeepAlive 组件的实例增加两个方法
    instance._deActive = vnode => {
      move(vnode, storageContainer);
    };
    instance._active = (vnode, container, anchor) => {
      move(vnode, container, anchor);
    };

    return () => {
      // keepAlive 的默认插槽就是要被缓存的组件
      let rawVNode = slot.default();
      // 不是组件类型的直接返回,因为其无法被缓存
      if (typeof rawVNode !== 'object') {
        return rawVNode;
      }

      // 挂载时,优先去获取被缓存组件的 vnode
      const catchVNode = cache.get(rawVNode.type);
      if (catchVNode) {
        rawVNode.component = catchVNode.component;
        // 避免渲染器重新挂载它
        rawVNode.keptAlive = true;
      } else {
        // 如果没有缓存,就将其加入到缓存,一般是组件第一次挂载
        cache.set(rawVNode.type, rawVNode);
      }
      // 避免渲染器真的把组件卸载,方便特殊处理
      rawVNode.shouldKeepAlive = true;
      rawVNode.keepAliveInstance = instance;
      return rawVNode;
    };
  }
};

从上可以看到,KeepAlive 组件不会渲染额外的内容,它的 render 函数最终只返回了要被缓存的组件(我们称要被缓存的组件为“内部组件”)。KeepAlive 会对“内部组件”操作,主要是在其 vnode 上添加一些特殊标记,从而使渲染器能够据此执行特殊的逻辑。

function patch(n1, n2, container) {
  if (n1 && n1.type !== n2.type) {
    unmount(n1);
    n1 = null;
  }
  const { type } = n2;
  if (type === 'string') {
    /** 执行普通的标签 patch */
  } else if (type === Text) {
    /** 处理文本节点 */
  } else if (type === Fragment) {
    /** 处理Fragment节点 */
  } else if (typeof type === 'object') {
    if (!n1) {
      if (n2.keptAlive) {
        n2.keepAliveInstance._activate(n2, container, anchor);
      } else {
        mountComponent(n2, container, anchor);
      }
    } else {
      patchComponent(n1, n2, anchor);
    }
  }
}

function unmount(vnode) {
  const { type } = vnode;
  if (type === Fragment) {
    /**... */
  } else if (typeof type === 'object') {
    if (vnode.shouldKeepAlive) {
      vnode.keepAliveInstance._deActivate(vnode);
    } else {
      unmount(vnode.component.subTree);
    }
    return;
  }
}

从上面的代码我们可以看出,vue 在渲染 KeepAlive 包裹的组件时,如果有缓存过将执行 keepAliveInstance._activate,在卸载时将执行 keepAliveInstance._deActivate

原因

通过上面的了解,我们知道,KeepAlive 缓存的是 vnode 节点,vnode 上面会有对应的真实DOM。组件“销毁”时,会将真实 DOM 移动到“隐藏容器”中,组件重新“渲染”时会从 vnode 上取到真实 DOM,再重新插入到页面中。这样对普通元素是没有影响的,但是 iframe 很特别,当其插入到页面时会重新加载,这是浏览器特性,与 Vue 无关。

解决方案

思路:路由第一次加载时将 iframe 渲染到页面中,路由切换时通过 v-show 改变显/隐。

  1. 在路由注册时,将 component 赋值为一个空组件
  {
    path: "/chathub",
    name: "chathub",
    component: { render() {} }, // 这里写 null 时控制台会出 warning,提示缺少 render 函数
  },
  1. 在 router-view 处,渲染 iframe,通过 v-show 来控制显示隐藏
  <ChatHub v-if="chatHubVisited" v-show="isChatHubPage"></ChatHub>
  <router-view v-slot="{ Component }">
    <keep-alive :include="keepAliveList">
      <component :is="Component"></component>
    </keep-alive>
  </router-view>
  1. 监听路由的变化,改变 iframe 的显/隐
const isChatHubPage = ref(false)
// 这里是个优化,想的是只有页面访问过该路由才渲染,没访问过就不渲染该组件
const chatHubVisited = ref(false) 

watch(
  () => routes.path,
  (value) => {
    if (value === '/chathub') {
      chatHubVisited.value = true
      isChatHubPage.value = true
    } else {
      isChatHubPage.value = false
    }
  },
  {
    immediate: true
  }
)