vue3?keepalive源碼解析解決線上問題
引言
- 1、通過本文可以了解到vue3 keepalive功能
- 2、通過本文可以了解到vue3 keepalive使用場景
- 3、通過本文可以學習到vue3 keepalive真實的使用過程
- 4、通過本文可以學習vue3 keepalive源碼調試
- 5、通過本文可以學習到vue3 keepalive源碼的精簡分析
1、keepalive功能
- keepalive是vue3中的一個全局組件
- keepalive 本身不會渲染出來,也不會出現在dom節(jié)點當中,但是它會被渲染為vnode,通過vnode可以跟蹤到keepalive中的cache和keys,當然也是在開發(fā)環(huán)境才可以,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組件,通過這里可以發(fā)現,我這里緩存的相當于整個頁面,當然你也可以進行更細粒度的控制頁面當中的某個區(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ā)現,通過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, 此時組件生命周期還沒開始。如果都已經開始執(zhí)行組件生命周期了,再寫入就意義了。
所以這個鉤子函數就不能寫在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
// 如果是在開發(fā)環(huán)境中可以查看到cache對象
instance.vnode.parent.__v_cache
// vue源碼中,在dev環(huán)境對cache進行暴露,生產環(huán)境是看不到的
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"
參考 http://www.dhdzp.com/article/154583.htm
5、執(zhí)行pnpm run dev則會build vue源碼
pnpm run dev
//則會出現以下,代表成功了(2022年5月27日),后期vue源代碼作者可能會更新,相應的提示可能發(fā)生變更,請注意一下
> @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便可以找到這個組件,然后通過左側行標右鍵就可以添加斷點,進行調試,也可以通過右側的【調用堆?!窟M行快速跳轉代碼進行調試。
<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、調試源碼發(fā)現 keepalive中的render函數(或者說時setup中的return 函數)在子組件切換時就會去執(zhí)行,變更邏輯緩存
- 第一次進入頁面初始化keepalive組件會執(zhí)行一次,
- 然后點擊組件一,再次執(zhí)行render函數
- 然后點擊組件二,會再次執(zhí)行render函數
8、調試截圖說明

9、調試操作,小視頻觀看

5、vue3 keealive源碼粗淺分析
通過查看vue3 KeepAlive.ts源碼,源碼路徑:https://github.com/vuejs/core/blob/main/packages/runtime-core/src/components/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
// 創(chuàng)建了隱藏容器
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變化時,對緩存進行動態(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ā)現通過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)
// 組件卸載的時候,對緩存列表進行循環(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)
})
})
// 同時在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)
// 節(jié)點類型
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
6、總結
通過這次查看vue3 keepalive源碼發(fā)現,其實也沒那么難,當然這次查看源代碼也只是粗略查看,并沒有看的那么細,主要還是先解決問題。動動手調試一下,有時候真的就是不逼一下自己都不知道自己有多么的優(yōu)秀。原來我也能稍微看看源代碼了。以后有空可以多看看vue3源代碼,學習一下vue3的精髓。了解vue3更為細節(jié)的一些知識點。
本文涉及到的代碼后續(xù)會整理到該代碼倉庫中
https://github.com/aehyok/vue-qiankun
最后自己每天工作中的筆記記錄倉庫,主要以文章鏈接和問題處理方案為主
https://github.com/aehyok/2022
以上就是vue3 keepalive源碼解析解決線上問題的詳細內容,更多關于vue3 keepalive的資料請關注腳本之家其它相關文章!
相關文章
關于Vue?"__ob__:Observer"屬性的解決方案詳析
在操作數據的時候發(fā)現,__ob__: Observer這個屬性出現之后,如果單獨拿數據的值,就會返回undefined,下面這篇文章主要給大家介紹了關于Vue?"__ob__:Observer"屬性的解決方案,需要的朋友可以參考下2022-11-11
vue中循環(huán)表格數據出現數據聯動現象(示例代碼)
在Vue中循環(huán)生成表格數據時,可能會遇到數據聯動的現象,即修改一個表格中的數據后,其他表格的數據也會跟著變化,這種現象通常是因為所有表格的數據引用了同一個對象或數組導致的,本文介紹vue中循環(huán)表格數據出現數據聯動現象,感興趣的朋友一起看看吧2024-11-11
vue深度監(jiān)聽(監(jiān)聽對象和數組的改變)與立即執(zhí)行監(jiān)聽實例
這篇文章主要介紹了vue深度監(jiān)聽(監(jiān)聽對象和數組的改變)與立即執(zhí)行監(jiān)聽實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09

