feb-alive
在剖析feb-alive實現之前,希望大家對以下基本知識有一定的瞭解。
- keep-alive實現原理
- history api
- vue渲染原理
- vue虛擬dom原理
feb-alive與keep-alive差異性
1. 針對activated鉤子差異性
keep-alive配合vue-router在動態路由切換的情況下不會觸發activated鉤子
,因為切換的時候元件沒有變化,所以只能通過beforeRouteUpdate
鉤子或者監聽$route
來實現資料更新,而feb-alive在動態路由切換時,依然會觸發activated鉤子,所以使用者可以放心的將業務更新邏輯寫在activated鉤子,不必關心動態路由還是非動態路由的情況。
2. feb-alive是頁面級快取,而keep-alive是元件級別快取
所以在上文中講到的使用keep-alive存在的一些限制問題都能夠得到有效的解決
實現原理
首先我們的目標很明確,需要開發的是一個頁面級別
的快取外掛,之前使用keep-alive遇到的諸多問題,歸根結底是因為它是一個元件級別
的快取。那麼我們就需要尋找每個頁面的特徵,用來儲存我們需要儲存的路由元件vnode,這裡我們就需要思考什麼可以作為每個頁面的標記
兩種方式:
- 通過每個url的查詢引數來儲存key
- 通過history.state來儲存key
方案一:使用查詢引數
優點:
- 可以相容vue-router的hash模式
缺點:
- 每個頁面的url後面都會帶一個查詢引數
- 每次頁面跳轉都需要重寫url
方案二:使用history.state
優點:
- 無需附帶額外的查詢引數
缺點:
- 不支援hash模式
相比方案一明顯的缺點,我更較傾向於方案二,捨棄hash模式的相容性,換來整個外掛更加好的使用者體驗效果。
接下來看下feb-alive的實現,feb-alive元件與上文的keep-alive一樣都是抽象元件,結構基本一致,主要區別在於render函式的實現
// 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中寫入資料
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
// 內層feb-alive例項會被儲存,防止從/home/a 跳轉到 /other的時候內層feb-alive執行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])) {
// 巢狀路由跳轉 && 父級路由
// /home/a --> /home/b
// 父路由通過key進行復用
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 {
// 巢狀路由跳轉 && 子路由
// 正常跳轉 && 動態路由跳轉
// /a --> /b
// /page/1 --> /page/2
vnode.key = `__febAlive-${key}-${vnode.tag}`
cacheVnode = getCacheVnode(cache, key)
// 只有相同的vnode才允許複用元件例項,否則雖然例項複用了,但是在patch的最後階段,會將複用的dom刪除
if (cacheVnode && vnode.tag === cacheVnode.tag) {
// 從普通路由後退到巢狀路由時,才需要復原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
}
複製程式碼
幾個關鍵的點都加上了註釋,現在我們一步一步解析
const vnode = this.$slots.default ? this.$slots.default[0] : null
const disableCache = this.$route.meta.disableCache
複製程式碼
此處與上一篇文章分析keep-alive實現一樣,在feb-alive元件的render函式中可以通過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中寫入資料
if (!history.state) {
return vnode
}
// 指定不使用快取
if (disableCache) {
return vnode
}
複製程式碼
首先判斷了當前宿主環境是否支援history。之後判斷當前頁面的history.state是否存在對應的頁面key,如果沒有則建立,並通過history.replaceState進行key值寫入。
最後又做了一層history.state判斷,因為有些瀏覽器不支援history的寫入操作。
當宿主環境不支援history的時候直接返回vnode。
當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的時候內層feb-alive執行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函式中計算了當前的feb-alive所在層級,這是為了解決巢狀路由的使用。
每個層級的feb-alive元件例項都維護著當前所在層級的路由元件例項的快取。這樣設計,feb-alive元件只需要關心自身所處層級的情況即可,減少了快取路由例項的成本。繼續分析程式碼
if (from.matched[depth] === to.matched[depth] && depth !== to.matched.length - 1) {
// ...
} else {
// ...
}
複製程式碼
Q: 這裡的if條件什麼時候成立呢?
答案:被包裹元件是巢狀路由中的父級路由元件
例如/home/a -> /home/b,其中home元件
在巢狀路由跳轉時不應該重新例項化,因為巢狀路由跳轉的時候,父路由元件狀態應該被儲存,而複用home元件,無需主動設定componentInstance,直接進行key設定複用即可
這裡需要重點關注下父元件例項快取的技巧
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)
}
複製程式碼
我們一步步分析
當我們首次訪問/home/a的時候,home元件對應的是層級為0,也就是最外層的feb-alive需要快取的vnode物件,這裡姑且用feb-alive[0]
來描述,此時cache[key]取到為undefined,cacheVnode也是undefined,這樣會進入到else邏輯,將home元件的vnode快取到cache[key]中。
當我們從/home/a 跳轉到 /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賦給當前的home元件的vnode即可,之後Vue在渲染的時候會通過key複用例項。從而保證/home/a -> /home/b 時,會複用home元件例項。
這樣我們就實現了巢狀路由中父級路由的複用。
其他情況的話就會走else邏輯
1. 普通路由跳轉
/foo -> /bar
複製程式碼
2. 動態路由跳轉
/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一樣,非常簡單
// 根據規則拼接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)
}
複製程式碼
此處根據key獲取到快取vnode,如果存在則複用例項並重新整理key的順序,否則快取當前的vnode,供下次快取恢復使用。
到此,feb-alive核心邏輯闡述完畢。