ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

解读keep-alive:Vue3中手动清理keep-alive组件缓存的一个解决方案

2022-09-16 14:00:45  阅读:240  来源: 互联网

标签:const name cache alive vnode instance key Vue3 keep


  用过vue的同学肯定对keep-alive组件不陌生,它允许我们使用key对组件进行缓存,当使用相同key的组件渲染时,就会使用缓存中的组件,这样可以加快渲染速度,特别是在使用路由跳转时,效果是很明显的,而缓存就意味着更多的内存消耗,但是很遗憾,keep-alive组件不允许我们手动释放,我们唯一能操作keep-alive组件的的地方就是三个属性:  

	interface KeepAliveProps {
		/**
		* 如果指定,则只有与 `include` 名称
		* 匹配的组件才会被缓存。
		*/
		include?: MatchPattern
		/**
		* 任何名称与 `exclude`
		* 匹配的组件都不会被缓存。
		*/
		exclude?: MatchPattern
		/**
		* 最多可以缓存多少组件实例。
		*/
		max?: number | string
	}
	
	type MatchPattern = string | RegExp | (string | RegExp)[]

  文档地址:https://cn.vuejs.org/api/built-in-components.html#keepalive

  想要随心所欲的清理keep-alive组件的缓存,显然这三个属性时满足不了我们的要求的。

  源码

  我们先看看keep-alive相关的源码:

KeepAlive相关部分源码
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 }) {
        const instance = getCurrentInstance();
        // KeepAlive communicates with the instantiated renderer via the
        // ctx where the renderer passes in its internals,
        // and the KeepAlive instance exposes activate/deactivate implementations.
        // The whole point of this is to avoid importing KeepAlive directly in the
        // renderer to facilitate tree-shaking.
        const sharedContext = instance.ctx;
        // if the internal renderer is not registered, it indicates that this is server-side rendering,
        // for KeepAlive, we just need to render its children
        if (!sharedContext.renderer) {
            return () => {
                const children = slots.default && slots.default();
                return children && children.length === 1 ? children[0] : children;
            };
        }
        const cache = new Map();
        const keys = new Set();
        let current = null;
        if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
            instance.__v_cache = cache;
        }
        const parentSuspense = instance.suspense;
        const { renderer: { p: patch, m: move, um: _unmount, o: { createElement } } } = sharedContext;
        const storageContainer = createElement('div');
        sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
            const instance = vnode.component;
            move(vnode, container, anchor, 0 /* MoveType.ENTER */, parentSuspense);
            // in case props have changed
            patch(instance.vnode, vnode, container, anchor, instance, parentSuspense, isSVG, vnode.slotScopeIds, optimized);
            queuePostRenderEffect(() => {
                instance.isDeactivated = false;
                if (instance.a) {
                    invokeArrayFns(instance.a);
                }
                const vnodeHook = vnode.props && vnode.props.onVnodeMounted;
                if (vnodeHook) {
                    invokeVNodeHook(vnodeHook, instance.parent, vnode);
                }
            }, parentSuspense);
            if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
                // Update components tree
                devtoolsComponentAdded(instance);
            }
        };
        sharedContext.deactivate = (vnode) => {
            const instance = vnode.component;
            move(vnode, storageContainer, null, 1 /* MoveType.LEAVE */, parentSuspense);
            queuePostRenderEffect(() => {
                if (instance.da) {
                    invokeArrayFns(instance.da);
                }
                const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted;
                if (vnodeHook) {
                    invokeVNodeHook(vnodeHook, instance.parent, vnode);
                }
                instance.isDeactivated = true;
            }, parentSuspense);
            if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
                // Update components tree
                devtoolsComponentAdded(instance);
            }
        };
        function unmount(vnode) {
            // reset the shapeFlag so it can be properly unmounted
            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 || cached.type !== current.type) {
                unmount(cached);
            }
            else if (current) {
                // current active instance should no longer be kept-alive.
                // we can't unmount it now but it might be later, so reset its flag now.
                resetShapeFlag(current);
            }
            cache.delete(key);
            keys.delete(key);
        }
        // prune cache on include/exclude prop change
        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 });
        // cache sub tree after render
        let pendingCacheKey = null;
        const cacheSubtree = () => {
            // fix #1621, the pendingCacheKey could be 0
            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) {
                    // current instance will be unmounted as part of keep-alive's unmount
                    resetShapeFlag(vnode);
                    // but invoke its deactivated hook here
                    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) {
                if ((process.env.NODE_ENV !== 'production')) {
                    warn(`KeepAlive should contain exactly one component child.`);
                }
                current = null;
                return children;
            }
            else if (!isVNode(rawVNode) ||
                (!(rawVNode.shapeFlag & 4 /* ShapeFlags.STATEFUL_COMPONENT */) &&
                    !(rawVNode.shapeFlag & 128 /* ShapeFlags.SUSPENSE */))) {
                current = null;
                return rawVNode;
            }
            let vnode = getInnerChild(rawVNode);
            const comp = vnode.type;
            // for async components, name check should be based in its loaded
            // inner component if available
            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);
            // clone vnode if it's reused because we are going to mutate it
            if (vnode.el) {
                vnode = cloneVNode(vnode);
                if (rawVNode.shapeFlag & 128 /* ShapeFlags.SUSPENSE */) {
                    rawVNode.ssContent = vnode;
                }
            }
            // #1513 it's possible for the returned vnode to be cloned due to attr
            // fallthrough or scopeId, so the vnode here may not be the final vnode
            // that is mounted. Instead of caching it directly, we store the pending
            // key and cache `instance.subTree` (the normalized vnode) in
            // beforeMount/beforeUpdate hooks.
            pendingCacheKey = key;
            if (cachedVNode) {
                // copy over mounted state
                vnode.el = cachedVNode.el;
                vnode.component = cachedVNode.component;
                if (vnode.transition) {
                    // recursively update transition hooks on subTree
                    setTransitionHooks(vnode, vnode.transition);
                }
                // avoid vnode being mounted as fresh
                vnode.shapeFlag |= 512 /* ShapeFlags.COMPONENT_KEPT_ALIVE */;
                // make this key the freshest
                keys.delete(key);
                keys.add(key);
            }
            else {
                keys.add(key);
                // prune oldest entry
                if (max && keys.size > parseInt(max, 10)) {
                    pruneCacheEntry(keys.values().next().value);
                }
            }
            // avoid vnode being unmounted
            vnode.shapeFlag |= 256 /* ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE */;
            current = vnode;
            return isSuspense(rawVNode.type) ? rawVNode : vnode;
        };
    }
};

function resetShapeFlag(vnode) {
    let shapeFlag = vnode.shapeFlag;
    if (shapeFlag & 256 /* ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE */) {
        shapeFlag -= 256 /* ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE */;
    }
    if (shapeFlag & 512 /* ShapeFlags.COMPONENT_KEPT_ALIVE */) {
        shapeFlag -= 512 /* ShapeFlags.COMPONENT_KEPT_ALIVE */;
    }
    vnode.shapeFlag = shapeFlag;
}

function getComponentName(Component, includeInferred = true) {
    return isFunction(Component)
        ? Component.displayName || Component.name
        : Component.name || (includeInferred && Component.__name);
}

function matches(pattern, name) {
    if (isArray(pattern)) {
        return pattern.some((p) => matches(p, name));
    }
    else if (isString(pattern)) {
        return pattern.split(',').includes(name);
    }
    else if (pattern.test) {
        return pattern.test(name);
    }
    /* istanbul ignore next */
    return false;
}

  查看源码可以发现,max、include、exclude三个属性的分别用在这几个地方:

  max的作用在渲染函数的最后:  

    if (cachedVNode) {
        // copy over mounted state
        vnode.el = cachedVNode.el;
        vnode.component = cachedVNode.component;
        if (vnode.transition) {
            // recursively update transition hooks on subTree
            setTransitionHooks(vnode, vnode.transition);
        }
        // avoid vnode being mounted as fresh
        vnode.shapeFlag |= 512 /* ShapeFlags.COMPONENT_KEPT_ALIVE */;
        // make this key the freshest
        keys.delete(key);
        keys.add(key);
    }
    else {
        keys.add(key);
        // prune oldest entry
        if (max && keys.size > parseInt(max, 10)) {
            pruneCacheEntry(keys.values().next().value);
        }
    }

  可以看到,如果没有设置max,keep-alive组件默认只缓存10个实例,而keys是一个Set对象,它不停地进行delete和add,就是调整组件的访问顺序,也就是说keys中最开头的那个就是最久未被访问的,这样当缓存达到上限后,就可以直接弹出第一个key进行释放。注意,这里仅仅是释放一个缓存,也就是说,如果开始时max=10,当缓存组件达到10个后,然后程序中将max设置成5,这时keep-alive组件只会清理一个缓存,也就是说,还有9个缓存组件!如果要将缓存组件数降下来,我们只能先设置max=9,然后设置max=8。。。。

  include、exclude属性作用在两个地方,一个是在渲染函数中,主要用于判断是否需要进行缓存:  

    let vnode = getInnerChild(rawVNode);
    const comp = vnode.type;
    // for async components, name check should be based in its loaded
    // inner component if available
    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;
    }

  一个是在watch函数中,主要是监听include、exclude来调整缓存组件状态,需要注意的是,watch中flush设置为post:

    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 || cached.type !== current.type) {
            unmount(cached);
        }
        else if (current) {
            // current active instance should no longer be kept-alive.
            // we can't unmount it now but it might be later, so reset its flag now.
            resetShapeFlag(current);
        }
        cache.delete(key);
        keys.delete(key);
    }
    // prune cache on include/exclude prop change
    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 });

  从上面的源码可以看到,无论在渲染函数中还是watch函数中,都需要使用两个重要的函数:getComponentName和matches  

	function getComponentName(Component, includeInferred = true) {
		return isFunction(Component)
			? Component.displayName || Component.name
			: Component.name || (includeInferred && Component.__name);
	}
	
	function matches(pattern, name) {
		if (isArray(pattern)) {
			return pattern.some((p) => matches(p, name));
		}
		else if (isString(pattern)) {
			return pattern.split(',').includes(name);
		}
		else if (pattern.test) {
			return pattern.test(name);
		}
		/* istanbul ignore next */
		return false;
	}

  getComponentName函数中的Component参数其实就是选项式Api中的 this.$.type 对象,或者组合式Api中setup中的 getCurrentInstance().type 对象,而这个type其实是组件的类型对象,同一个组件的多个实例共享同一个type对象,getComponentName函数中优先取它的name属性,没有则取 __name 属性。

  matches函数中的pattern参数可以是一个数组、逗号隔开的字符串、包含一个test函数的对象(不一定是ReExp对象,也可以是一个继承ReExp对象的自定义类型)。

  手动释放的一个解决方案

  从上面的源码可以看到,keep-alive组件的三个属性在能起到一些作用,如果想随心所欲的来清理keep-alive缓存,这三个属性就不够用了。

  除此之外,在源码中可以看到,keep-alive的缓存保存在一个Map对象中:

    const cache = new Map();
    const keys = new Set();
    let current = null;
    if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
        instance.__v_cache = cache;
    }

  可以看到,这个缓存对象两种情况下会挂载到keep-alive组件中:

    1、process.env.NODE_ENV !== 'production':表明它是作用在开发环境下
    2、__VUE_PROD_DEVTOOLS__:表示是否启用了devtool

  对于process.env.NODE_ENV变量:

  如果是webpack4,可以在.env文件中设置NODE_ENV变量值进行覆盖。

  如果是webpack5,貌似它的值就不能改动了,一般在开发环境中,它的值是development,build之后就是production

  对于__VUE_PROD_DEVTOOLS__变量,我们只需要启用即可,添加vue.config.js,内容如下:  

    module.exports = {
      configureWebpack: {
        devtool: "source-map",  //设置成false表示关闭
      },
      chainWebpack: (config) => {
        config.plugin("define").tap((definitions) => {
          for (const definition of definitions) {
            if (definition) {
              Object.assign(definition, {
                __VUE_OPTIONS_API__: true, 
                __VUE_PROD_DEVTOOLS__: true, 
              });
            }
          }
          return definitions;
        });
      }
    };

  将缓存Map对象挂载到keep-alive组件之后,我们就可以通过$refs取到keep-alive组件对象,进而得到缓存Map对象了。

  为了方便使用,我们可以创建一个类来简化操作:  

handler.js
export class KeepAliveHandler {
  constructor() {
    this._ = {};
  }

  get keys() {
    const { cache } = this._
    if (!cache || !cache()) {
      return [];
    }
    return [...cache().keys()];
  }

  //绑定keepAlive信息
  bind(keepAlive) {
    if (keepAlive && keepAlive.$.__v_cache) {
      const sharedContext = keepAlive.$.ctx;
      const instance = keepAlive.$;
      const { suspense: parentSuspense, __v_cache: cache } = instance;
      const {
        renderer: { um: unmount },
      } = sharedContext;

      Object.assign(this._, {
        cache() {
          return cache;
        },
        unmount(vnode) {
          resetShapeFlag(vnode);
          unmount(vnode, instance, parentSuspense, true);
        },
        isCurrent(key) {
          return keepAlive.$.subTree && keepAlive.$.subTree.key === key
        }
      });
    } else {
      console.warn('当且仅当开发环境或者启用了devtool时生效')
    }
  }

  //删除指定key的缓存
  remove(key, reset = true) {
    pruneCache.call(this, k => key !== k, reset)
  }
  //清空
  clear() {
    pruneCache.call(this, () => false, false)
  }
}

function pruneCache(filter, reset) {
  const { cache, unmount, isCurrent } = this._
  if (!cache || !cache()) {
    return
  }
  const c = cache()
  c.set = new Map().set
  c.forEach((vnode, key) => {
    if (!filter(key)) {
      if (isCurrent(key)) {
        //重写set,因为渲染函数可能会重新执行
        //这样就会导致缓存重新添加,导致清除失败
        if (reset) {
          c.set = function () {
            c.set = new Map().set
          }
        }
        resetShapeFlag(vnode)
      } else {
        unmount(vnode);
      }
      c.delete(key);
    }
  });
}

function resetShapeFlag(vnode) {
  let shapeFlag = vnode.shapeFlag;
  if (shapeFlag & 256) {
    shapeFlag -= 256;
  }
  if (shapeFlag & 512) {
    shapeFlag -= 512;
  }
  vnode.shapeFlag = shapeFlag;
}

   一个简单的例子:

  创建一个vue3的项目,包含vue-router,然后创建两个vue组件:  

home.vue
 <template>
  <div class="home">
    <h1>This is an home page</h1>
    <h1>route:{{ $route.fullPath }}</h1>
    <h1>time:{{ now }}</h1>
  </div>
</template>

<script>
export default {
  name: "Home",
  data() {
    return {
      now: new Date().toLocaleString(),
    };
  },
  mounted() {
    console.log("home mounted");
  },
  unmounted() {
    console.log("home unmounted");
  },
};
</script>
about.vue
 <template>
  <div class="about">
    <h1>This is an about page</h1>
    <h1>route:{{ $route.fullPath }}</h1>
    <h1>time:{{ now }}</h1>
  </div>
</template>

<script>
export default {
  name: "About",
  data() {
    return {
      now: new Date().toLocaleString(),
    };
  },
  mounted() {
    console.log("about mounted");
  },
  unmounted() {
    console.log("about unmounted");
  },
};
</script>

  vue-router注册路由:  

route.js
 import { createRouter, createWebHistory } from "vue-router";
import Home from "../views/Home.vue";

const routes = [
  {
    path: "/",
    name: "Home",
    component: Home,
  },
  {
    path: "/about",
    name: "About",
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/About.vue"),
  },
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
});

export default router;

  如果不使用keep-alive,App.vue的内容如下:

    <template>
      <div>
        <div id="nav">
          <router-link to="/">Home</router-link> |
          <router-link to="/about">About</router-link>
        </div>
        <router-view></router-view>
      </div>
    </template>

    <style>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
    }

    #nav {
      padding: 30px;
    }

    #nav a {
      font-weight: bold;
      color: #2c3e50;
      cursor: pointer;
    }

    #nav a.router-link-exact-active {
      color: #42b983;
    }
    </style>

  这时运行后,来回切换home和about页面,会发现两个页面每次都是重新渲染,其中的now属性一直在获取当前最新的时间,如果开发者工具中看控制台输出,会发现里面不停的打印:about mounted、about unmounted、home mounted、home unmounted。

  

  如果使用keep-alive后,那么修改App.vue:

<template>
      <div>
        <div id="nav">
          <router-link to="/">Home</router-link>
          (<a href="#" @click="remove('/')">x</a>)|
          <router-link to="/about">About</router-link>
          (<a href="#" @click="remove('/about')">x</a>)
        </div>
        <router-view v-slot="{ Component }">
          <keep-alive ref="keepAlive">
            <component :is="Component" :key="$route.fullPath"></component>
          </keep-alive>
        </router-view>
      </div>
    </template>

    <script>
    import { KeepAliveHandler } from "@/handler";
    import { onMounted, getCurrentInstance } from "vue";

    export default {
      setup() {
        const instance = getCurrentInstance();
        const handler = new KeepAliveHandler();
        onMounted(() => {
          const keepAlive = instance.refs.keepAlive;
          handler.bind(keepAlive);
        });
        const remove = (key) => {
          handler.remove(key);
        };

        return {
          remove,
        };
      },
    };
    </script>
    <style>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
    }

    #nav {
      padding: 30px;
    }

    #nav a {
      font-weight: bold;
      color: #2c3e50;
      cursor: pointer;
    }

    #nav a.router-link-exact-active {
      color: #42b983;
    }
    </style>

  这里先创建一个KeepAliveHandler实例,然后在onMounted钩子中进行keep-alive组件的绑定,接下来就可以使用KeepAliveHandler实例的remove函数来删除指定key的缓存组件了。

  项目运行后,来回切换home和about页面,会发现页面中的时间没有更新了,而且组件的mounted也只调用了一次,这就是keep-alive组件缓存机制在起作用。

  接着,分别点击home和about旁边的 x ,再来回切换home和about页面,会发现页面中的事件更新了一次,而且组件对应的unmounted也执行了,这就说明成功清理了keep-alive的缓存,来回切换页面时重新渲染了页面并缓存

  结语

  其实keep-alive组件手动释放的问题由来已久,从原来vue2就开始有这个问题了,但是不知道为何vue的作者一直没有修复,github上也有不少吐槽这点的issue。

  虽然这样子可以解决手动释放keep-alive缓存的问题,但是需要production环境启用devtools或者将process.env.NODE_ENV设置成非production,但是遗憾的事,在生产环境,我们恰恰是相反的,所以这种不是一个好的解决方案,只是利用vue给我们开的一扇窗子而已。

  其次,经过测试,__VUE_PROD_DEVTOOLS__可以覆盖,但有时候又覆盖不了,貌似是版本的问题,我这边暂时使用的是webpack,有兴趣的可以使用vite试试,具体是可以参考github上给出的说明:https://github.com/vuejs/core/tree/main/packages/vue#bundler-build-feature-flags  

  后续有时间在仔细看看源码,看看有没有可以钻空子的地方。

 

标签:const,name,cache,alive,vnode,instance,key,Vue3,keep
来源: https://www.cnblogs.com/shanfeng1000/p/16692266.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有