Vue頁面級緩存解決方案feb-alive (下)

feb-alive

Vue頁面級緩存解決方案feb-alive (上)html

在剖析feb-alive實現以前,但願你們對如下基本知識有必定的瞭解。vue

  • keep-alive實現原理
  • history api
  • vue渲染原理
  • vue虛擬dom原理

feb-alive與keep-alive差別性

1. 針對activated鉤子差別性

keep-alive配合vue-router在動態路由切換的狀況下不會觸發activated鉤子,由於切換的時候組件沒有變化,因此只能經過beforeRouteUpdate鉤子或者監聽$route來實現數據更新,而feb-alive在動態路由切換時,依然會觸發activated鉤子,因此用戶能夠放心的將業務更新邏輯寫在activated鉤子,沒必要關心動態路由仍是非動態路由的狀況。html5

2. feb-alive是頁面級緩存,而keep-alive是組件級別緩存

因此在上文中講到的使用keep-alive存在的一些限制問題都可以獲得有效的解決node

實現原理

首先咱們的目標很明確,須要開發的是一個頁面級別的緩存插件,以前使用keep-alive遇到的諸多問題,歸根結底是由於它是一個組件級別的緩存。那麼咱們就須要尋找每一個頁面的特徵,用來存儲咱們須要存儲的路由組件vnode,這裏咱們就須要思考什麼能夠做爲每一個頁面的標記git

兩種方式:github

  1. 經過每一個url的查詢參數來存儲key
  2. 經過history.state來存儲key

方案一:使用查詢參數vue-router

優勢:api

  • 能夠兼容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所在層級,這是爲了解決嵌套路由的使用。

avatar
每一個層級的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核心邏輯闡述完畢。

參考文檔

相關文章
相關標籤/搜索