淺談Vue頁面級緩存解決方案feb-alive (下)
feb-alive
在剖析feb-alive實現(xiàn)之前,希望大家對以下基本知識有一定的了解。
- keep-alive實現(xiàn)原理
- history api
- vue渲染原理
- vue虛擬dom原理
feb-alive與keep-alive差異性
1. 針對activated鉤子差異性
keep-alive配合vue-router在動態(tài)路由切換的情況下不會觸發(fā)activated鉤子,因為切換的時候組件沒有變化,所以只能通過beforeRouteUpdate鉤子或者監(jiān)聽$route來實現(xiàn)數(shù)據(jù)更新,而feb-alive在動態(tài)路由切換時,依然會觸發(fā)activated鉤子,所以用戶可以放心的將業(yè)務(wù)更新邏輯寫在activated鉤子,不必關(guān)心動態(tài)路由還是非動態(tài)路由的情況。
2. feb-alive是頁面級緩存,而keep-alive是組件級別緩存
所以在上文中講到的使用keep-alive存在的一些限制問題都能夠得到有效的解決
實現(xiàn)原理
首先我們的目標(biāo)很明確,需要開發(fā)的是一個頁面級別的緩存插件,之前使用keep-alive遇到的諸多問題,歸根結(jié)底是因為它是一個組件級別的緩存。那么我們就需要尋找每個頁面的特征,用來存儲我們需要存儲的路由組件vnode,這里我們就需要思考什么可以作為每個頁面的標(biāo)記
兩種方式:
- 通過每個url的查詢參數(shù)來存儲key
- 通過history.state來存儲key
方案一:使用查詢參數(shù)
優(yōu)點:
可以兼容vue-router的hash模式
缺點:
每個頁面的url后面都會帶一個查詢參數(shù)
每次頁面跳轉(zhuǎn)都需要重寫url
方案二:使用history.state
優(yōu)點:
無需附帶額外的查詢參數(shù)
缺點:
不支持hash模式
相比方案一明顯的缺點,我更較傾向于方案二,舍棄hash模式的兼容性,換來整個插件更加好的用戶體驗效果。
接下來看下feb-alive的實現(xiàn),feb-alive組件與上文的keep-alive一樣都是抽象組件,結(jié)構(gòu)基本一致,主要區(qū)別在于render函數(shù)的
實現(xiàn)
// feb-alive/src/components/feb-alive.js
render () {
// 取到router-view的vnode
const vnode = this.$slots.default ? this.$slots.default[0] : null
const disableCache = this.$route.meta.disableCache
// 如果不支持html5 history則不做緩存處理
if (!supportHistoryState) {
return vnode
}
// 嘗試寫入key
if (!history.state || !history.state[keyName]) {
const state = {
[keyName]: genKey()
}
const path = getLocation()
history.replaceState(state, null, path)
}
// 有些瀏覽器不支持往state中寫入數(shù)據(jù)
if (!history.state) {
return vnode
}
// 指定不使用緩存
if (disableCache) {
return vnode
}
// 核心邏輯
if (vnode) {
const { cache, keys } = this
const key = history.state[keyName]
const { from, to } = this.$router.febRecord
let parent = this.$parent
let depth = 0
let cacheVnode = Object.create(null)
vnode && (vnode.data.febAlive = true)
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.febAlive) {
depth++
}
parent = parent.$parent
}
// 記錄緩存及其所在層級
febCache[depth] = cache
// /home/a backTo /other
// 內(nèi)層feb-alive實例會被保存,防止從/home/a 跳轉(zhuǎn)到 /other的時候內(nèi)層feb-alive執(zhí)行render時候,多生成一個實例
if (to.matched.length < depth + 1) {
return null
}
if (from.matched[depth] === to.matched[depth] && (from.matched.slice(-1)[0] !== to.matched.slice(-1)[0])) {
// 嵌套路由跳轉(zhuǎn) && 父級路由
// /home/a --> /home/b
// 父路由通過key進行復(fù)用
cache[key] = cache[key] || this.keys[this.keys.length - 1]
cacheVnode = getCacheVnode(cache, cache[key])
if (cacheVnode) {
vnode.key = cacheVnode.key
remove(keys, key)
keys.push(key)
} else {
this.cacheClear()
cache[key] = vnode
keys.push(key)
}
} else {
// 嵌套路由跳轉(zhuǎn) && 子路由
// 正常跳轉(zhuǎn) && 動態(tài)路由跳轉(zhuǎn)
// /a --> /b
// /page/1 --> /page/2
vnode.key = `__febAlive-${key}-${vnode.tag}`
cacheVnode = getCacheVnode(cache, key)
// 只有相同的vnode才允許復(fù)用組件實例,否則雖然實例復(fù)用了,但是在patch的最后階段,會將復(fù)用的dom刪除
if (cacheVnode && vnode.tag === cacheVnode.tag) {
// 從普通路由后退到嵌套路由時,才需要復(fù)原key
vnode.key = cacheVnode.key
vnode.componentInstance = cacheVnode.componentInstance
remove(keys, key)
keys.push(key)
} else {
this.cacheClear()
cache[key] = vnode
keys.push(key)
}
}
vnode.data.keepAlive = true
}
return vnode
}
幾個關(guān)鍵的點都加上了注釋,現(xiàn)在我們一步一步解析
const vnode = this.$slots.default ? this.$slots.default[0] : null const disableCache = this.$route.meta.disableCache
此處與上一篇文章分析keep-alive實現(xiàn)一樣,在feb-alive組件的render函數(shù)中可以通過this.$slots.default[0]獲取到嵌套的第一個默認插槽的vnode,也就是router-view組件vnode,同時獲取到了路由配置disableCache用來判斷用戶是否配置改頁面啟用緩存。
// 如果不支持html5 history 寫操作則不做緩存處理
if (!supportHistoryState) {
return vnode
}
// 嘗試寫入key
if (!history.state || !history.state[keyName]) {
const state = {
[keyName]: genKey()
}
const path = getLocation()
history.replaceState(state, null, path)
}
// 有些瀏覽器不支持往state中寫入數(shù)據(jù)
if (!history.state) {
return vnode
}
// 指定不使用緩存
if (disableCache) {
return vnode
}
首先判斷了當(dāng)前宿主環(huán)境是否支持history。之后判斷當(dāng)前頁面的history.state是否存在對應(yīng)的頁面key,如果沒有則創(chuàng)建,并通過history.replaceState進行key值寫入。
最后又做了一層history.state判斷,因為有些瀏覽器不支持history的寫入操作。
當(dāng)宿主環(huán)境不支持history的時候直接返回vnode。
當(dāng)route.meta.disableCache為true時,也直接返回vnode
// 核心邏輯
if (vnode) {
const { cache, keys } = this
const key = history.state[keyName]
const { from, to } = this.$router.febRecord
let parent = this.$parent
let depth = 0
let cacheVnode = Object.create(null)
vnode && (vnode.data.febAlive = true)
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.febAlive) {
depth++
}
parent = parent.$parent
}
// 記錄緩存及其所在層級
febCache[depth] = cache
// /home/a backTo /other
// 由于feb-alive實例會被保存,防止例如/home/a 后退到 /other的時候內(nèi)層feb-alive執(zhí)行render時候,多生成一個實例
if (to.matched.length < depth + 1) {
return null
}
if (from.matched[depth] === to.matched[depth] && (from.matched.slice(-1)[0] !== to.matched.slice(-1)[0])) {
// ...
} else {
// ...
}
vnode.data.keepAlive = true
}
首先,我們在每個feb-alive組件的render函數(shù)中計算了當(dāng)前的feb-alive所在層級,這是為了解決嵌套路由的使用。

每個層級的feb-alive組件實例都維護著當(dāng)前所在層級的路由組件實例的緩存。這樣設(shè)計,feb-alive組件只需要關(guān)心自身所處層級的情況即可,減少了緩存路由實例的成本。
繼續(xù)分析代碼
if (from.matched[depth] === to.matched[depth] && depth !== to.matched.length - 1) {
// ...
} else {
// ...
}
Q: 這里的if條件什么時候成立呢?
答案:被包裹組件是嵌套路由中的父級路由組件
例如/home/a -> /home/b,其中home組件在嵌套路由跳轉(zhuǎn)時不應(yīng)該重新實例化,因為嵌套路由跳轉(zhuǎn)的時候,父路由組件狀態(tài)應(yīng)該被保存,而復(fù)用home組件,無需主動設(shè)置componentInstance,直接進行key設(shè)置復(fù)用即可
這里需要重點關(guān)注下父組件實例緩存的技巧
cache[key] = cache[key] || this.keys[this.keys.length - 1]
cacheVnode = getCacheVnode(cache, cache[key])
if (cacheVnode) {
vnode.key = cacheVnode.key
remove(keys, key)
keys.push(key)
} else {
this.cacheClear()
cache[key] = vnode
keys.push(key)
}
我們一步步分析
當(dāng)我們首次訪問/home/a的時候,home組件對應(yīng)的是層級為0,也就是最外層的feb-alive需要緩存的vnode對象,這里姑且用feb-alive[0]來描述,此時cache[key]取到為undefined,cacheVnode也是undefined,這樣會進入到else邏輯,將home組件的vnode緩存到cache[key]中。
當(dāng)我們從/home/a 跳轉(zhuǎn)到 /home/b 時,針對home組件會再次進入到上面的代碼片段
// 取到的是/home/a頁面的key cache[key] = cache[key] || this.keys[this.keys.length - 1]
取到的是/home/a頁面的key,所以之后cacheVnode就可以取到/home/a頁面訪問時存儲的home組件的vnode,這個時候只需要將其key賦給當(dāng)前的home組件的vnode即可,之后Vue在渲染的時候會通過key復(fù)用實例。從而保證/home/a -> /home/b 時,會復(fù)用home組件實例。
這樣我們就實現(xiàn)了嵌套路由中父級路由的復(fù)用。
其他情況的話就會走else邏輯
1. 普通路由跳轉(zhuǎn)
/foo -> /bar
2. 動態(tài)路由跳轉(zhuǎn)
/page/1 -> /page/2
3. 嵌套路由中的子級路由
/home/foo -> /home/bar 中的foo, bar組件 /home/foo/a -> /home/bar/a 中的foo, bar組件,注意a組件依然會走if邏輯,不過其操作沒有太大意義 /home/page/1 -> /home/page/2 中的page組件
針對else這層邏輯和keep-alive一樣,非常簡單
// 根據(jù)規(guī)則拼接vnode key
vnode.key = `__febAlive-${key}-${vnode.tag}`
// 獲取緩存vnode
cacheVnode = getCacheVnode(cache, key)
// 判斷是否命中緩存vnode,此處還必須保證兩個vnode的tag相同
if (cacheVnode && vnode.tag === cacheVnode.tag) {
vnode.key = cacheVnode.key
vnode.componentInstance = cacheVnode.componentInstance
remove(keys, key)
keys.push(key)
} else {
this.cacheClear()
cache[key] = vnode
keys.push(key)
}
此處根據(jù)key獲取到緩存vnode,如果存在則復(fù)用實例并刷新key的順序,否則緩存當(dāng)前的vnode,供下次緩存恢復(fù)使用。
到此,feb-alive核心邏輯闡述完畢。
參考文檔
vue-navigation
Vue.js 技術(shù)揭秘
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
vant遇到van-sidebar數(shù)據(jù)超出不能滑動的問題
這篇文章主要介紹了vant遇到van-sidebar數(shù)據(jù)超出不能滑動的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-04-04
詳解Vue3中shallowRef和shallowReactive的使用
這篇文章主要為大家介紹了Vue3中shallowRef和shallowReactive函數(shù)的使用方法,文中的示例代碼講解詳細,對我們學(xué)習(xí)Vue有一定的幫助,需要的可以參考一下2022-07-07
vue中$event使用之獲取當(dāng)前元素及相關(guān)元素
這篇文章主要介紹了vue中$event使用之獲取當(dāng)前元素及相關(guān)元素,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-10-10
vue中的路由跳轉(zhuǎn)tabBar圖片和文字的高亮效果
這篇文章主要介紹了vue中的路由跳轉(zhuǎn)tabBar圖片和文字的高亮效果,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-10-10
vue?element?ui表格相同數(shù)據(jù)合并單元格效果實例
工作中遇到需要根據(jù)單元格某個屬性合并,特此記錄下,下面這篇文章主要給大家介紹了關(guān)于vue?element?ui表格相同數(shù)據(jù)合并單元格效果的相關(guān)資料,文中通過圖文介紹的非常詳細,需要的朋友可以參考下2023-11-11
解決Vue響應(yīng)式數(shù)據(jù)已獲取而視圖不更新的問題
這篇文章主要介紹了解決Vue響應(yīng)式數(shù)據(jù)已獲取而視圖不更新的問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08
Vue模擬響應(yīng)式原理底層代碼實現(xiàn)的示例
最近去面試的人都會有這個體會,去年面試官只問我怎么用vue,今年開始問我vue響應(yīng)式原理,本文就詳細的介紹一下2021-08-08

