Vue中 keep-alive的实现原理分析

keep-alive的实现原理

使用vue的时候,想必大家都是用过keep-alive,其作用就是缓存页面以及其状态。使用了这么久vue只知道如何使用但不明白其中原理,昨天翻看实现代码,这里做个笔记,其中有些注释是我自己加上的,便于理解。

这里以vue3为例

整个组件的源码为:

const KeepAliveImpl = {
    name: `KeepAlive`,
    // Marker for special handling inside the renderer. We are not using a ===
    // check directly on KeepAlive in the renderer, because importing it directly
    // would prevent it from being tree-shaken.
    __isKeepAlive: true,
    props: {
      include: [String, RegExp, Array],
      exclude: [String, RegExp, Array],
      max: [String, Number]
    },
    setup(props, { slots }) {
      // getCurrentInstance 是 Vue 3.x 中 Composition API 的一个常用钩子函数,它可以在组件函数内获取当前组件实例的上下文,便于我们使用其它 Composition API。
      // 我们可以通过 getCurrentInstance这个函数来返回当前组件的实例对象,也就是当前vue这个实例对象
// Vue2中,可以通过this来获取当前组件实例;
// Vue3中,在setup中无法通过this获取组件实例,console.log(this)打印出来的值是undefined。

      const instance = getCurrentInstance();
      // instance.ctx是获得当前组件实例的上下文
      const sharedContext = instance.ctx;
      const cache = /* @__PURE__ */ new Map();
      const keys = /* @__PURE__ */ new Set();
      let current = null;
      {
        instance.__v_cache = cache;
      }
      // 目前instance.suspense这个定义的是
      const parentSuspense = instance.suspense;
      const {
        renderer: {
          p: patch,
          m: move,
          um: _unmount,
          o: { createElement }
        }
      } = sharedContext;
      const storageContainer = createElement("div");
      sharedContext.activate = (vnode, container, anchor, namespace, optimized) => {
        const instance2 = vnode.component;
        move(vnode, container, anchor, 0, parentSuspense);
        patch(
          instance2.vnode,
          vnode,
          container,
          anchor,
          instance2,
          parentSuspense,
          namespace,
          vnode.slotScopeIds,
          optimized
        );
        queuePostRenderEffect(() => {
          instance2.isDeactivated = false;
          if (instance2.a) {
            invokeArrayFns(instance2.a);
          }
          const vnodeHook = vnode.props && vnode.props.onVnodeMounted;
          if (vnodeHook) {
            invokeVNodeHook(vnodeHook, instance2.parent, vnode);
          }
        }, parentSuspense);
        {
          devtoolsComponentAdded(instance2);
        }
      };
      sharedContext.deactivate = (vnode) => {
        const instance2 = vnode.component;
        move(vnode, storageContainer, null, 1, parentSuspense);
        queuePostRenderEffect(() => {
          if (instance2.da) {
            invokeArrayFns(instance2.da);
          }
          const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted;
          if (vnodeHook) {
            invokeVNodeHook(vnodeHook, instance2.parent, vnode);
          }
          instance2.isDeactivated = true;
        }, parentSuspense);
        {
          devtoolsComponentAdded(instance2);
        }
      };
      function unmount(vnode) {
        resetShapeFlag(vnode);
        _unmount(vnode, instance, parentSuspense, true);
      }
      function pruneCache(filter) {
        cache.forEach((vnode, key) => {
          const name = getComponentName(vnode.type);
          if (name && (!filter || !filter(name))) {
            pruneCacheEntry(key);
          }
        });
      }
      function pruneCacheEntry(key) {
        const cached = cache.get(key);
        if (!current || !isSameVNodeType(cached, current)) {
          unmount(cached);
        } else if (current) {
          resetShapeFlag(current);
        }
        cache.delete(key);
        keys.delete(key);
      }
      watch(
        () => [props.include, props.exclude],
        ([include, exclude]) => {
          include && pruneCache((name) => matches(include, name));
          exclude && pruneCache((name) => !matches(exclude, name));
        },
        // prune post-render after `current` has been updated
        { flush: "post", deep: true }
      );
      let pendingCacheKey = null;
      const cacheSubtree = () => {
        if (pendingCacheKey != null) {
          cache.set(pendingCacheKey, getInnerChild(instance.subTree));
        }
      };
      onMounted(cacheSubtree);
      onUpdated(cacheSubtree);
      onBeforeUnmount(() => {
        cache.forEach((cached) => {
          const { subTree, suspense } = instance;
          const vnode = getInnerChild(subTree);
          if (cached.type === vnode.type && cached.key === vnode.key) {
            resetShapeFlag(vnode);
            const da = vnode.component.da;
            da && queuePostRenderEffect(da, suspense);
            return;
          }
          unmount(cached);
        });
      });
      return () => {
        pendingCacheKey = null;
        if (!slots.default) {
          return null;
        }
        const children = slots.default();
        const rawVNode = children[0];
        if (children.length > 1) {
          {
            warn$1(`KeepAlive should contain exactly one component child.`);
          }
          current = null;
          return children;
        } else if (!isVNode(rawVNode) || !(rawVNode.shapeFlag & 4) && !(rawVNode.shapeFlag & 128)) {
          current = null;
          return rawVNode;
        }
        let vnode = getInnerChild(rawVNode);
        const comp = vnode.type;
        const name = getComponentName(
          isAsyncWrapper(vnode) ? vnode.type.__asyncResolved || {} : comp
        );
        const { include, exclude, max } = props;
        if (include && (!name || !matches(include, name)) || exclude && name && matches(exclude, name)) {
          current = vnode;
          return rawVNode;
        }
        const key = vnode.key == null ? comp : vnode.key;
        const cachedVNode = cache.get(key);
        if (vnode.el) {
          vnode = cloneVNode(vnode);
          if (rawVNode.shapeFlag & 128) {
            rawVNode.ssContent = vnode;
          }
        }
        pendingCacheKey = key;
        if (cachedVNode) {
          vnode.el = cachedVNode.el;
          vnode.component = cachedVNode.component;
          if (vnode.transition) {
            setTransitionHooks(vnode, vnode.transition);
          }
          vnode.shapeFlag |= 512;
          keys.delete(key);
          keys.add(key);
        } else {
          keys.add(key);
          if (max && keys.size > parseInt(max, 10)) {
            pruneCacheEntry(keys.values().next().value);
          }
        }
        vnode.shapeFlag |= 256;
        current = vnode;
        return isSuspense(rawVNode.type) ? rawVNode : vnode;
      };
    }
  };

很容易看出keep-alive其实就是vue自己封装的一个组件,和普通组件一样。

再讲keep-alive组件前先了解下vue组件的整个渲染

大致流程如下
Vue中 keep-alive的实现原理分析_第1张图片

keep-alive生命周期

组件挂载:
调用setupStatefulComponent函数触发组件setup方法,其中组件的setup方法核心代码其实就几行:

return () => {
    const children = slots.default()
    let vnode = children[0]
    cache.set(key, vnode)
 
    if (cached) {
      vnode.el = cached.el
      vnode.anchor = cached.anchor
      vnode.component = cached.component
      vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
      keys.delete(key)
      keys.add(key) 
    } else {
      keys.add(key)
    }
    return vnode
}
主要逻辑为三:
  • 1.确认需要渲染的slot、
  • 2.将其状态置入缓存或读取已存在的缓存、
  • 3.返回slot对应的vnode,紧接着调用setupRenderEffect,渲染出dom。
组件更新(slot变化):

当slot变化后,首先会调用keep-alive组件的render即setup的返回函数,逻辑见上面setup方法。紧接着当某个slot卸载时,会调用deactivate函数,当某个slot重新挂载时,则会调用activate函数,核心代码如下:

const storageContainer = createElement('div')
sink.activate = (vnode, container, anchor) => {
      move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
      queuePostRenderEffect(() => {
        const component = vnode.component!
        component.isDeactivated = false
        if (component.a !== null) {
          invokeHooks(component.a)
        }
      }, parentSuspense)
    }
 
    sink.deactivate = (vnode: VNode) => {
      move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
      queuePostRenderEffect(() => {
        const component = vnode.component!
        if (component.da !== null) {
          invokeHooks(component.da)
        }
        component.isDeactivated = true
      }, parentSuspense)
    }

逻辑也很简单,当组件卸载时,将其移入缓存的dom节点中,调用slot的deactivate生命周期,当组件重新挂载时候,将其移入至挂载的dom节点中。

总结来说,keep-alive实现原理就是将对应的状态放入一个cache对象中,对应的dom节点放入缓存dom中,当下次再次需要渲染时,从对象中获取状态,从缓存dom中移出至挂载dom节点中。

keep-alive的使用总结

在平常开发中,有些组件只需要加载一次,后面的数据将不存在变化,亦或者是组件需要缓存状态,滚动条位置等,这个时候,keep-alive的用处就立刻凸显出来了。

1.App.vue中使用keep-alive

include表示需要缓存的页面,exclude表示不需要缓存的页面,你可以只设置其中一个即可,但两个同时设置的时候,切记exclude优先级高于include,例如a组件在exclude中和include中都存在,那么,a组件是不会被缓存的。

<template>
    <div id="app">
      <keep-alive :include="whiteList" :exclude="blackList">
        <router-view  v-if="isRouterAlive" >router-view>
      keep-alive>
    div>
template>
<script>
export default {
    name: 'App',
    data(){
      return{
          isRouterAlive:true,
          whiteList:['styleLibrary','OrderList','SalesData'],
          blackList:['Footer'],
          personShow:false,
      }
    },
}
script>
2.App.vue中配合router进行使用
<template>
 <div id="app">
  <keep-alive>
    <router-view v-if="$route.meta.keepAlive">router-view>    
  keep-alive>
  <router-view v-if="!$route.meta.keepAlive">router-view>      
 div>
template>

将需要缓存的组件的$route.meta中的keepAlive设置为true,反之为false

{
      path:'/login',
      name:'login',
      component:resolve=>require(['@/pages/login'],resolve),
      meta:{
        keepAlive:true,
        title:'登录',
        savedPosition:true,
      }
 }

你可能感兴趣的:(vue3.x,keep-alive,keep-alive的原理)