Vue3小菜:缓存组件keep-alive实现

211 阅读3分钟

组件缓存

简介

  • 作用:容器组件,无实际意义,被包裹的组件可视为插槽。插槽会被缓存起来,不会重复渲染
  • 本质:
    • 是一个插槽组件h(component,null,{default:()=>h(component)}
    • 默认取插槽的第一个孩子节点,vue3中规定一个keep-alive容器只能存在一个孩子default-slot
  • 使用示例
        // template
        <keep-alive :include="cacheList">
            <component
            :is="Component"
            :key="route.path"
            class="router-component"
            />
        </keep-alive>
        // JS
        const My1 = {
            setup(){
                onMounted(()=>{
                    console.log('my1')
                })
            },
            render:h('h1','my1')
        }
        render(h(KeepAlive,null,{
            default:()=>h(My1)
        }))
    

原理简述

  • 缓存虚拟节点数据,并创建真实DOM元素碎片保存被切换掉的组件DOM,当切换回来时,从元素中提取出来并放置被切换的组件DOM
  • 通过绑定的key值与容器渲染的组件绑定

注意:

  • slot指的是组件的虚拟节点,并非真实的DOM,每次切换vnode.component => subTree
  • 只有当容器组件渲染完时,才能确定渲染的结果subTree
    • 容器初次渲染时在容器组件的onMounted钩子中将key值与组件绑定。
    • 切换时在容器的onUpdated钩子中将新的key值与组件绑定。
  • 组件实例切换时
    • 旧组件实例加标识ShapeFlag.COMPONENT_SHOULD_KEEP_ALIVE,不走卸载,而是直接将实例的子节点subTree移入缓存中Map对象,并触发deactived钩子
    • 新组件渲染,并触发actived钩子
      • 若新组件实例在缓存对象中,则加标识ShapeFlag.COMPONENT_KEPT_ALIVE,不走创建,直接从缓存的DOM元素中拿出来
      • 若不在缓存对象中,则创建并渲染。
  • 属性说明
    • includes要缓存的组件名称
    • excludes不需要缓存的组件名称
    • max最大缓存的组件个数,防止缓存对象过大
      • 缓存策略LRU算法:队列中越先进且越久没被反复用到的,优先删除。跟先进先出差不多,不过复用时会将其刷新为最新的。
    • component.name声明组件时自定义的名称
    • 根据规则字符串匹配、正则匹配等方式,将组件实例的名称与传入的属性做对比,符合的就缓存这也是模板名称的意义,如果不需要缓存则没有意义
import { isVnode } from "./vnode";
import { getCurrentInstance } from "./component";
import { ShapeFlags } from "@vue/shared";
import { onMount, updated } from "./apiLifecycle";

export const keepAliveImpl = {
  __isKeepAlive: true,
  props: {
    include: {},
    exclude: {},
    max: {},
  },
  setup(props, { slots }) {
    const keys = new Set(); // 缓存的key
    const cache = new Map(); //哪个key,对应的虚拟节点

    const instance = getCurrentInstance(); // 单线程

    // createElement创建节点的API,move:移动节点的API
    let { createElement, move } = instance.ctx.renderer;
    //创建真实节点容器,但不插入页面,用于缓存实例的真实节点
    let storageContainer = createElement("div"); 

    // 失活:卸载时调用此方法,将实例孩子移入缓存容器
    instance.ctx.deactivate = function (vnode) {
      move(vnode, storageContainer);
    };

    // 激活:缓存中的组件实例重新被拿出来时调用,不用重复渲染
    instance.ctx.activate = function (vnode, container, achor) {
      move(vnode, container, achor);
    };

    // key值占位符,因为需要等组件渲染完才绑定key与组件的关系[异步]
    let pendingKey = null; 

    function cacheSubTree() {
      if (pendingKey) {
        //绑定组件实例孩子与key值
        cache.set(pendingKey, instance.subTree); 
      }
    }
    // 挂载时绑定、更新时绑定
    onMount(cacheSubTree);
    updated(cacheSubTree);

    // 组件传入的属性
    const { include, exclude, max } = props;
    // 实例组件的名字
    let current = null;

    return () => {
      let vnode = slots.default(); // 获取渲染的实例

      // 条件1:是虚拟节点。条件2:是组件且有状态
      if (
        !isVnode(vnode) ||
        !(vnode.shapeFlags & ShapeFlags.STATEFUL_COMPONENT)
      ) {
        return;
      }

      const comp = vnode.type;
      // 如果实例没有key,则以实例作为map对象的key
      const key = vnode.key == null ? comp : vnode.key; 
      let name = comp.name; // 实例的名字,用于对比
      if (
        (name && !include.split(",").includes(name)) ||
        (exclude && exclude.split(",").includes(name))
      ) {
        return vnode;
      }

      let cacheVnode = cache.get(key); // 看看是否被缓存过
      if (cacheVnode) {
        // 被缓存过,直接复用
        vnode.component = cacheVnode.component;
         //标识实例,不要重新创建了,复用即可
        vnode.shapeFlags |= ShapeFlags.COMPONENT_KEPT_ALIVE;
      } else {
        keys.add(key); // 进入缓存key,容器实例更改、挂载时绑定
        pendingKey = key;
        if (max && keys.size > max) {
          // 拿出缓存区的前面第一个删掉
        }
        // 标识实例,告诉卸载函数,不要卸载,移动一下就好
        vnode.shapeFlags |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE; 
        current = vnode;
      }
      return vnode;
    };
  },
};