keepalive 本身不会渲染出来,也不会出现在dom节点当中,但是它会被渲染为vnode,通过vnode可以跟踪到keepalive中的cache和keys,当然也是在开发环境才可以,build打包以后没有暴露到vnode中(这个还要再确认一下)
keepalive 最重要的功能就是缓存组件
keepalive 通过lru缓存淘汰策略来更新组件缓存,可以更有效的利用内存,防止内存溢出,源代码中的最大缓存数max为10,也就是10个组件之后,就开始淘汰最先被缓存的组件了
2、keepalive使用场景这里先假设一个场景: a页面是首页=====> b页面列表页面(需要缓存的页面)=======> c 详情页 由c详情页到到b页面的时候,要返回到b的缓存页面,包括页面的基础数据和列表的滚动条位置信息 如果由b页面返回到a页面,则需要将b的缓存页清空
上述另外一个场景:进入页面直接缓存,然后就结束了,这个比较简单本文就不讨论了
3、在项目中的使用过程
keepalive组件总共有三个参数
include:可传字符串、正则表达式、数组,名称匹配成功的组件会被缓存
exclude:可传字符串、正则表达式、数组,名称匹配成功的组件不会被缓存
max:可传数字,限制缓存组件的最大数量,默认为10
首先在app.vue根代码中添加引入keepalive组件,通过这里可以发现,我这里缓存的相当于整个页面,当然你也可以进行更细粒度的控制页面当中的某个区域组件
<template> <router-view v-slot="{ component }"> <keep-alive :include="keepalivecache"> <component :is="component" :key="$route.name" /> </keep-alive> </router-view> </template> <script lang="ts" setup> import { computed } from "vue"; import { usekeepaliverstore } from "@/store"; const usestore = usekeepaliverstore(); const keepalivecache = computed(() => { return usestore.caches; }); </script>
通过app.vue可以发现,通过pinia(也就是vue2中使用的vuex)保存要缓存的页面组件, 来处理include缓存,和保存页面组件中的滚动条信息数据
import { definestore } from "pinia"; export const usekeepaliverstore = definestore("usekeepaliverstore", { state: () => ({ caches: [] as any, scrolllist: new map(), // 缓存页面组件如果又滚动条的高度 }), actions: { add(name: string) { this.caches.push(name); }, remove(name: string) { console.log(this.caches, 'this.caches') this.caches = this.caches.filter((item: any) => item !== name); console.log(this.caches, 'this.caches') }, clear() { this.caches = [] } } });
组件路由刚刚切换时,通过beforerouteenter将组件写入include, 此时组件生命周期还没开始。如果都已经开始执行组件生命周期了,再写入就意义了。
所以这个钩子函数就不能写在setup中,要单独提出来写。当然你也可以换成路由的其他钩子函数处理beforeeach,但这里面使用的话,好像使用不了pinia,这个还需要进一步研究一下。
import { useroute, userouter, onbeforerouteleave } from "vue-router"; import { usekeepaliverstore } from "@/store"; const usestore = usekeepaliverstore() export default { name:"record-month", beforerouteenter(to, from, next) { next(vm => { if(from.name === 'home' && to.name === 'record-month') { usestore.add(to.name) } }); } } </script>
组件路由离开时判断,是否要移出缓存,这个钩子就直接写在setup中就可以了。
onbeforerouteleave((to, from) => { console.log(to.name, "onbeforerouteleave"); if (to.name === "new-detection-detail") { console.log(to, from, "进入详情页面不做处理"); } else { usestore.remove(from.name) console.log(to, from, "删除组件缓存"); } });
在keepalive两个钩子函数中进行处理scroll位置的缓存,onactivated中获取缓存中的位置, ondeactivated记录位置到缓存
onactivated(() => { if(usestore.scrolllist.get(routename)) { const top = usestore.scrolllist.get(routename) reflist.value.setscrolltop(number(top)) } }); ondeactivated(() => { const top = reflist.value.getscrolltop() usestore.scrolllist.set(routename, top) });
这里定义一个方法,设置scrolltop使用了原生javascript的api
const setscrolltop = (value: any) => { const dom = document.queryselector('.van-pull-refresh') dom!.scrolltop = value }
同时高度怎么获取要先注册scroll事件,然后通过getscrolltop 获取当前滚动条的位置进行保存即可
onmounted(() => { scrolldom.value = document.queryselector('.van-pull-refresh') as htmlelement const throttledfun = usethrottlefn(() => { console.log(scrolldom.value?.scrolltop, 'addeventlistener') state.scrolltop = scrolldom.value!.scrolltop }, 500) if(scrolldom.value) { scrolldom.value.addeventlistener('scroll',throttledfun) } }) const getscrolltop = () => { console.log('scrolldom.vaue', scrolldom.value?.scrolltop) return state.scrolltop }
上面注册scroll事件中使用了一个usethrottlefn,这个类库是@vueuse/core中提供的,其中封装了很多工具都非常不错,用兴趣的可以研究研究
https://vueuse.org/shared/usethrottlefn/#usethrottlefn
此时也可以查看找到实例的vnode查找到keepalive,是在keepalive紧挨着的子组件里
const instance = getcurrentinstance() console.log(instance.vnode.parent) // 这里便是keepalive组件vnode // 如果是在开发环境中可以查看到cache对象 instance.vnode.parent.__v_cache // vue源码中,在dev环境对cache进行暴露,生产环境是看不到的 if (__dev__ || __feature_prod_devtools__) { ;(instance as any).__v_cache = cache }
4、vue3 keepalive源码调试1、克隆代码
git clone git@github.com:vuejs/core.git
2、安装依赖
pnpm i
3、如果不能使用pnpm,可以先通过npm安装一下
npm i pnpm -g
4、安装完成以后,找到根目录package.json文件中的scripts
// 在dev命令后添加 --source-map是从已转换的代码,映射到原始的源文件 "dev": "node scripts/dev.js --sourcemap"
参考 https://www.yisu.com/article/154583.htm
5、执行pnpm run dev则会build vue源码
pnpm run dev //则会出现以下,代表成功了(2022年5月27日),后期vue源代码作者可能会更新,相应的提示可能发生变更,请注意一下 > @3.2.36 dev h:\github\sourcecode\core > node scripts/dev.js --sourcemap watching: packages\vue\dist\vue.global.js //到..\..\core\packages\vue\dist便可以看到编译成功,以及可以查看到examples样例demo页面
6、然后在 ....\core\packages\vue\examples\composition中添加一个aehyok.html文件,将如下代码进行拷贝,然后通过chrome浏览器打开,f12,找到源代码的tab页面,通过快捷键ctrl+ p 输入keepalive便可以找到这个组件,然后通过左侧行标右键就可以添加断点,进行调试,也可以通过右侧的【调用堆栈】进行快速跳转代码进行调试。
<script src="../../dist/vue.global.js"></script> <script type="text/x-template" id="template-1"> <div>template-1</div> <div>template-1</div> </script> <script type="text/x-template" id="template-2"> <div>template-2</div> <div>template-2</div> </script> <script> const { reactive, computed } = vue const demo1 = { name: 'demo1', template: '#template-1', setup(props) { } } const demo2 = { name: 'demo2', template: '#template-2', setup(props) { } } </script> <!-- app template (in dom) --> <div id="demo"> <div>hello world</div> <div>hello world</div> <div>hello world</div> <button @click="changeclick(1)">组件一</button> <button @click="changeclick(2)">组件二</button> <keep-alive :include="includecache"> <component :is="componentcache" :key="componentname" v-if="componentname" /> </keep-alive> </div> <!-- app script --> <script> vue.createapp({ components: { demo1, demo2 }, data: () => ({ includecache: [], componentcache: '', componentname: '', }), methods:{ changeclick(type) { if(type === 1) { if(!this.includecache.includes('demo1')) { this.includecache.push('demo1') } console.log(this.includecache, '000') this.componentcache = demo1 this.componentname = 'demo1' } if(type === 2) { if(!this.includecache.includes('demo2')) { this.includecache.push('demo2') } console.log(this.includecache, '2222') this.componentname = 'demo2' this.componentcache = demo2 } } } }).mount('#demo') </script>
7、调试源码发现 keepalive中的render函数(或者说时setup中的return 函数)在子组件切换时就会去执行,变更逻辑缓存
第一次进入页面初始化keepalive组件会执行一次,
然后点击组件一,再次执行render函数
然后点击组件二,会再次执行render函数
8、调试截图说明
5、vue3 keealive源码粗浅分析通过查看vue3 keepalive.ts源码
// 在setup初始化中,先获取keepalive实例 // getcurrentinstance() 可以获取当前组件的实例 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 as keepalivecontext // if the internal renderer is not registered, it indicates that this is server-side rendering, // for keepalive, we just need to render its children /// ssr 判断,暂时可以忽略掉即可。 if (__ssr__ && !sharedcontext.renderer) { return () => { const children = slots.default && slots.default() return children && children.length === 1 ? children[0] : children } } // 通过map存储缓存vnode, // 通过set存储缓存的key(在外面设置的key,或者vnode的type) const cache: cache = new map() const keys: keys = new set() let current: vnode | null = null if (__dev__ || __feature_prod_devtools__) { ;(instance as any).__v_cache = cache } const parentsuspense = instance.suspense const { renderer: { p: patch, m: move, um: _unmount, o: { createelement } } } = sharedcontext // 创建了隐藏容器 const storagecontainer = createelement('div') // 在实例上注册两个钩子函数 activate, deactivate sharedcontext.activate = (vnode, container, anchor, issvg, optimized) => { const instance = vnode.component! move(vnode, container, anchor, 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 (__dev__ || __feature_prod_devtools__) { // update components tree devtoolscomponentadded(instance) } } sharedcontext.deactivate = (vnode: vnode) => { const instance = vnode.component! move(vnode, storagecontainer, null, 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 (__dev__ || __feature_prod_devtools__) { // update components tree devtoolscomponentadded(instance) } } // 组件卸载 function unmount(vnode: vnode) { // reset the shapeflag so it can be properly unmounted resetshapeflag(vnode) _unmount(vnode, instance, parentsuspense, true) } // 定义 include和exclude变化时,对缓存进行动态处理 function prunecache(filter?: (name: string) => boolean) { cache.foreach((vnode, key) => { const name = getcomponentname(vnode.type as concretecomponent) if (name && (!filter || !filter(name))) { prunecacheentry(key) } }) } function prunecacheentry(key: cachekey) { const cached = cache.get(key) as vnode 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) } // 可以发现通过include 可以配置被显示的组件, // 当然也可以设置exclude来配置不被显示的组件, // 组件切换时随时控制缓存 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 } ) // 定义当前组件key // cache sub tree after render let pendingcachekey: cachekey | null = 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) }) }) // 同时在keepalive组件setup生命周期中,return () => {} 渲染的时候,对组件进行判断逻辑处理,同样对include和exclude判断渲染。 // 判断keepalive组件中的子组件,如果大于1个的话,直接警告处理了 // 另外如果渲染的不是虚拟dom(vnode),则直接返回渲染即可。 return () => { // eslint-disable-next-line no-debugger console.log(props.include, 'watch-include') pendingcachekey = null if (!slots.default) { return null } const children = slots.default() const rawvnode = children[0] if (children.length > 1) { if (__dev__) { warn(`keepalive should contain exactly one component child.`) } current = null return children } else if ( !isvnode(rawvnode) || (!(rawvnode.shapeflag & shapeflags.stateful_component) && !(rawvnode.shapeflag & shapeflags.suspense)) ) { current = null return rawvnode } // 接下来处理时vnode虚拟dom的情况,先获取vnode let vnode = getinnerchild(rawvnode) // 节点类型 const comp = vnode.type as concretecomponent // for async components, name check should be based in its loaded // inner component if available // 获取组件名称 const name = getcomponentname( isasyncwrapper(vnode) ? (vnode.type as componentoptions).__asyncresolved || {} : comp ) //这个算是最熟悉的通过props传递进行的参数,进行解构 const { include, exclude, max } = props // include判断 组件名称如果没有设置, 或者组件名称不在include中, // exclude判断 组件名称有了,或者匹配了 // 对以上两种情况都不进行缓存处理,直接返回当前vnode虚拟dom即可。 if ( (include && (!name || !matches(include, name))) || (exclude && name && matches(exclude, name)) ) { current = vnode return rawvnode } // 接下来开始处理有缓存或者要缓存的了 // 先获取一下vnode的key设置,然后看看cache缓存中是否存在 const key = vnode.key == null ? comp : vnode.key const cachedvnode = cache.get(key) // 这一段可以忽略了,好像时sscontent相关,暂时不管了,没看明白?? // clone vnode if it's reused because we are going to mutate it if (vnode.el) { vnode = clonevnode(vnode) if (rawvnode.shapeflag & shapeflags.suspense) { rawvnode.sscontent = vnode } } // 上面判断了,如果没有设置key,则使用vnode的type作为key值 pendingcachekey = key //判断上面缓存中是否存在vnode // if 存在的话,就将缓存中的vnode复制给当前的vnode // 同时还判断了组件是否为过渡组件 transition,如果是的话 需要注册过渡组件的钩子 // 同时先删除key,然后再重新添加key // else 不存在的话,就添加到缓存即可 // 并且要判断一下max最大缓存的数量是否超过了,超过了,则通过淘汰lpr算法,删除最旧的一个缓存 // 最后又判断了一下是否为suspense。也是vue3新增的高阶组件。 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 |= 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 as string, 10)) { prunecacheentry(keys.values().next().value) } } // avoid vnode being unmounted vnode.shapeflag |= shapeflags.component_should_keep_alive current = vnode return issuspense(rawvnode.type) ? rawvnode : vnode
以上就是vue3 keepalive线上问题怎么解决的详细内容。
