vue3?keepalive源碼解析解決線上問題
引言
- 1、通過本文可以了解到vue3 keepalive功能
- 2、通過本文可以了解到vue3 keepalive使用場(chǎng)景
- 3、通過本文可以學(xué)習(xí)到vue3 keepalive真實(shí)的使用過程
- 4、通過本文可以學(xué)習(xí)vue3 keepalive源碼調(diào)試
- 5、通過本文可以學(xué)習(xí)到vue3 keepalive源碼的精簡(jiǎn)分析
1、keepalive功能
- keepalive是vue3中的一個(gè)全局組件
- keepalive 本身不會(huì)渲染出來,也不會(huì)出現(xiàn)在dom節(jié)點(diǎn)當(dāng)中,但是它會(huì)被渲染為vnode,通過vnode可以跟蹤到keepalive中的cache和keys,當(dāng)然也是在開發(fā)環(huán)境才可以,build打包以后沒有暴露到vnode中(這個(gè)還要再確認(rèn)一下)
- keepalive 最重要的功能就是緩存組件
- keepalive 通過LRU緩存淘汰策略來更新組件緩存,可以更有效的利用內(nèi)存,防止內(nèi)存溢出,源代碼中的最大緩存數(shù)max為10,也就是10個(gè)組件之后,就開始淘汰最先被緩存的組件了
2、keepalive使用場(chǎng)景
- 這里先假設(shè)一個(gè)場(chǎng)景: A頁面是首頁=====> B頁面列表頁面(需要緩存的頁面)=======> C 詳情頁 由C詳情頁到到B頁面的時(shí)候,要返回到B的緩存頁面,包括頁面的基礎(chǔ)數(shù)據(jù)和列表的滾動(dòng)條位置信息 如果由B頁面返回到A頁面,則需要將B的緩存頁清空
- 上述另外一個(gè)場(chǎng)景:進(jìn)入頁面直接緩存,然后就結(jié)束了,這個(gè)比較簡(jiǎn)單本文就不討論了
3、在項(xiàng)目中的使用過程
keepalive組件總共有三個(gè)參數(shù)
- include:可傳字符串、正則表達(dá)式、數(shù)組,名稱匹配成功的組件會(huì)被緩存
- exclude:可傳字符串、正則表達(dá)式、數(shù)組,名稱匹配成功的組件不會(huì)被緩存
- max:可傳數(shù)字,限制緩存組件的最大數(shù)量,默認(rèn)為10
首先在App.vue根代碼中添加引入keepalive組件,通過這里可以發(fā)現(xiàn),我這里緩存的相當(dāng)于整個(gè)頁面,當(dāng)然你也可以進(jìn)行更細(xì)粒度的控制頁面當(dāng)中的某個(gè)區(qū)域組件
<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可以發(fā)現(xiàn),通過pinia(也就是vue2中使用的vuex)保存要緩存的頁面組件, 來處理include緩存,和保存頁面組件中的滾動(dòng)條信息數(shù)據(jù)
import { defineStore } from "pinia"; export const useKeepAliverStore = defineStore("useKeepAliverStore", { state: () => ({ caches: [] as any, scrollList: new Map(), // 緩存頁面組件如果又滾動(dòng)條的高度 }), 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 = [] } } });
組件路由剛剛切換時(shí),通過beforeRouteEnter將組件寫入include, 此時(shí)組件生命周期還沒開始。如果都已經(jīng)開始執(zhí)行組件生命周期了,再寫入就意義了。
所以這個(gè)鉤子函數(shù)就不能寫在setup中,要單獨(dú)提出來寫。當(dāng)然你也可以換成路由的其他鉤子函數(shù)處理beforeEach,但這里面使用的話,好像使用不了pinia,這個(gè)還需要進(jìn)一步研究一下。
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>
組件路由離開時(shí)判斷,是否要移出緩存,這個(gè)鉤子就直接寫在setup中就可以了。
onBeforeRouteLeave((to, from) => { console.log(to.name, "onBeforeRouteLeave"); if (to.name === "new-detection-detail") { console.log(to, from, "進(jìn)入詳情頁面不做處理"); } else { useStore.remove(from.name) console.log(to, from, "刪除組件緩存"); } });
在keepalive兩個(gè)鉤子函數(shù)中進(jìn)行處理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) });
這里定義一個(gè)方法,設(shè)置scrollTop使用了原生javascript的api
const setScrollTop = (value: any) => { const dom = document.querySelector('.van-pull-refresh') dom!.scrollTop = value }
同時(shí)高度怎么獲取要先注冊(cè)scroll事件,然后通過getScrollTop 獲取當(dāng)前滾動(dòng)條的位置進(jìn)行保存即可
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 }
上面注冊(cè)scroll事件中使用了一個(gè)useThrottleFn,這個(gè)類庫是@vueuse/core中提供的,其中封裝了很多工具都非常不錯(cuò),用興趣的可以研究研究
https://vueuse.org/shared/usethrottlefn/#usethrottlefn
此時(shí)也可以查看找到實(shí)例的vnode查找到keepalive,是在keepalive緊挨著的子組件里
const instance = getCurrentInstance() console.log(instance.vnode.parent) // 這里便是keepalive組件vnode // 如果是在開發(fā)環(huán)境中可以查看到cache對(duì)象 instance.vnode.parent.__v_cache // vue源碼中,在dev環(huán)境對(duì)cache進(jìn)行暴露,生產(chǎn)環(huán)境是看不到的 if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { ;(instance as any).__v_cache = cache }
4、vue3 keepalive源碼調(diào)試
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是從已轉(zhuǎn)換的代碼,映射到原始的源文件 "dev": "node scripts/dev.js --sourcemap"
參考 http://www.dbjr.com.cn/article/154583.htm
5、執(zhí)行pnpm run dev則會(huì)build vue源碼
pnpm run dev //則會(huì)出現(xiàn)以下,代表成功了(2022年5月27日),后期vue源代碼作者可能會(huì)更新,相應(yīng)的提示可能發(fā)生變更,請(qǐng)注意一下 > @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中添加一個(gè)aehyok.html文件,將如下代碼進(jìn)行拷貝,然后通過chrome瀏覽器打開,F(xiàn)12,找到源代碼的Tab頁面,通過快捷鍵Ctrl+ P 輸入KeepAlive便可以找到這個(gè)組件,然后通過左側(cè)行標(biāo)右鍵就可以添加斷點(diǎn),進(jìn)行調(diào)試,也可以通過右側(cè)的【調(diào)用堆?!窟M(jìn)行快速跳轉(zhuǎn)代碼進(jìn)行調(diào)試。
<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、調(diào)試源碼發(fā)現(xiàn) keepalive中的render函數(shù)(或者說時(shí)setup中的return 函數(shù))在子組件切換時(shí)就會(huì)去執(zhí)行,變更邏輯緩存
- 第一次進(jìn)入頁面初始化keepalive組件會(huì)執(zhí)行一次,
- 然后點(diǎn)擊組件一,再次執(zhí)行render函數(shù)
- 然后點(diǎn)擊組件二,會(huì)再次執(zhí)行render函數(shù)
8、調(diào)試截圖說明
9、調(diào)試操作,小視頻觀看
5、vue3 keealive源碼粗淺分析
通過查看vue3 KeepAlive.ts源碼,源碼路徑:https://github.com/vuejs/core/blob/main/packages/runtime-core/src/components/KeepAlive.ts
// 在setup初始化中,先獲取keepalive實(shí)例 // getCurrentInstance() 可以獲取當(dāng)前組件的實(shí)例 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 判斷,暫時(shí)可以忽略掉即可。 if (__SSR__ && !sharedContext.renderer) { return () => { const children = slots.default && slots.default() return children && children.length === 1 ? children[0] : children } } // 通過Map存儲(chǔ)緩存vnode, // 通過Set存儲(chǔ)緩存的key(在外面設(shè)置的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 // 創(chuàng)建了隱藏容器 const storageContainer = createElement('div') // 在實(shí)例上注冊(cè)兩個(gè)鉤子函數(shù) 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變化時(shí),對(duì)緩存進(jìn)行動(dòng)態(tài)處理 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) } // 可以發(fā)現(xiàn)通過include 可以配置被顯示的組件, // 當(dāng)然也可以設(shè)置exclude來配置不被顯示的組件, // 組件切換時(shí)隨時(shí)控制緩存 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 } ) // 定義當(dāng)前組件Key // cache sub tree after render let pendingCacheKey: CacheKey | null = null // 這是一個(gè)重要的方法,設(shè)置緩存 const cacheSubtree = () => { // fix #1621, the pendingCacheKey could be 0 if (pendingCacheKey != null) { cache.set(pendingCacheKey, getInnerChild(instance.subTree)) } } onMounted(cacheSubtree) onUpdated(cacheSubtree) // 組件卸載的時(shí)候,對(duì)緩存列表進(jìn)行循環(huán)判斷處理 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) }) }) // 同時(shí)在keepAlive組件setup生命周期中,return () => {} 渲染的時(shí)候,對(duì)組件進(jìn)行判斷邏輯處理,同樣對(duì)include和exclude判斷渲染。 // 判斷keepalive組件中的子組件,如果大于1個(gè)的話,直接警告處理了 // 另外如果渲染的不是虛擬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 } // 接下來處理時(shí)Vnode虛擬dom的情況,先獲取vnode let vnode = getInnerChild(rawVNode) // 節(jié)點(diǎn)類型 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 ) //這個(gè)算是最熟悉的通過props傳遞進(jìn)行的參數(shù),進(jìn)行解構(gòu) const { include, exclude, max } = props // include判斷 組件名稱如果沒有設(shè)置, 或者組件名稱不在include中, // exclude判斷 組件名稱有了,或者匹配了 // 對(duì)以上兩種情況都不進(jìn)行緩存處理,直接返回當(dāng)前vnode虛擬dom即可。 if ( (include && (!name || !matches(include, name))) || (exclude && name && matches(exclude, name)) ) { current = vnode return rawVNode } // 接下來開始處理有緩存或者要緩存的了 // 先獲取一下vnode的key設(shè)置,然后看看cache緩存中是否存在 const key = vnode.key == null ? comp : vnode.key const cachedVNode = cache.get(key) // 這一段可以忽略了,好像時(shí)ssContent相關(guān),暫時(shí)不管了,沒看明白?? // 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 } } // 上面判斷了,如果沒有設(shè)置key,則使用vNode的type作為key值 pendingCacheKey = key //判斷上面緩存中是否存在vNode // if 存在的話,就將緩存中的vnode復(fù)制給當(dāng)前的vnode // 同時(shí)還判斷了組件是否為過渡組件 transition,如果是的話 需要注冊(cè)過渡組件的鉤子 // 同時(shí)先刪除key,然后再重新添加key // else 不存在的話,就添加到緩存即可 // 并且要判斷一下max最大緩存的數(shù)量是否超過了,超過了,則通過淘汰LPR算法,刪除最舊的一個(gè)緩存 // 最后又判斷了一下是否為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
6、總結(jié)
通過這次查看vue3 keepalive源碼發(fā)現(xiàn),其實(shí)也沒那么難,當(dāng)然這次查看源代碼也只是粗略查看,并沒有看的那么細(xì),主要還是先解決問題。動(dòng)動(dòng)手調(diào)試一下,有時(shí)候真的就是不逼一下自己都不知道自己有多么的優(yōu)秀。原來我也能稍微看看源代碼了。以后有空可以多看看vue3源代碼,學(xué)習(xí)一下vue3的精髓。了解vue3更為細(xì)節(jié)的一些知識(shí)點(diǎn)。
本文涉及到的代碼后續(xù)會(huì)整理到該代碼倉庫中
https://github.com/aehyok/vue-qiankun
最后自己每天工作中的筆記記錄倉庫,主要以文章鏈接和問題處理方案為主
https://github.com/aehyok/2022
以上就是vue3 keepalive源碼解析解決線上問題的詳細(xì)內(nèi)容,更多關(guān)于vue3 keepalive的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
前端vue-cropperjs實(shí)現(xiàn)圖片裁剪方案
這篇文章主要為大家介紹了前端vue-cropperjs實(shí)現(xiàn)圖片裁剪方案,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07vue修改vue項(xiàng)目運(yùn)行端口號(hào)的方法
本篇文章主要介紹了vue修改vue項(xiàng)目運(yùn)行端口號(hào)的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-08-08Vue+ElementUI踩坑之動(dòng)態(tài)顯示/隱藏表格的列el-table-column問題
這篇文章主要介紹了Vue+ElementUI踩坑之動(dòng)態(tài)顯示/隱藏表格的列el-table-column問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11vue學(xué)習(xí)筆記之v-if和v-show的區(qū)別
本篇文章主要介紹了vue學(xué)習(xí)筆記之v-if和v-show的區(qū)別,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09element-ui組件table實(shí)現(xiàn)自定義篩選功能的示例代碼
這篇文章主要介紹了element-ui組件table實(shí)現(xiàn)自定義篩選功能的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-03-03vue清空數(shù)組的幾個(gè)方式(小結(jié))
本文主要介紹了vue清空數(shù)組的幾個(gè)方式,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12