在nuxt項目中使用keep-alive的兩種可能方案

關於Vue中keep-alive的作用,以及在部分場景下由於避免重複請求、重複渲染的性能提升,想必大家都很清楚了,在此不再贅述。

但是在nuxt項目中使用keep-alive就有個問題,比如我有一個用到嵌套路由的頁面,導航欄(或者標籤頁)由幾個頁面共享,然後根據路由切換頁面內容。這應該是個很常見的場景,Vue的文檔裏keep-alive的部分用的也是這個例子。爲了直觀,放個圖好了:
在這裏插入圖片描述
在這個場景下使用nuxt,雖然整個頁面沒有重複渲染(因爲被keep-alive緩存了),但是每次都會觸發asyncData這個鉤子,去服務端拿數據,這就是一個無謂的開銷,而且還會阻塞渲染。這個問題在nuxt的pull request #5947裏提到了,雖然最後被作者自己close掉了。

網上絕大部分的內容都沒有提到這一點,只說寫個<nuxt-child keep-alive />就行了,還是稍微有點粗暴了。

在此之前,我考慮過這樣幾種處理方式:

  1. asyncData裏使用緩存。但是,後來發現這種方案不太可行,因爲asyncData在服務端觸發,無法調用this,這意味着數據是無法緩存在組件裏的,比如拿到數據之後放在data裏,然後每次都從data裏讀數據。
  2. 使用mountedactivated鉤子,按照SPA的方式去寫。這麼寫是沒問題,但是如果我這麼寫的話,爲什麼要用nuxt來做SSR呢?因爲我的場景需要SEO,同時希望減少閃屏,所以並不能採用這種方式。

後來看到這位dalao寫了一種方案:https://juejin.im/post/5cff5f02e51d4510624f97ab,對我有很大的啓發,主要的思路是隻讓asyncData在服務端觸發,然後在瀏覽器端做一些數據的獲取。

但是反向思考一下,如果asyncData不能避免,那我能不能把每次調用的開銷降到最小呢?我們想一下,在一般的業務場景下,開銷最大的是什麼?顯然是ajax(不包括在asyncData裏進行密集計算的場景)。那我只要避免ajax,就可以降低調用開銷。所以思路就很明顯了,緩存。

但是剛纔也提到了,在asyncData裏使用緩存是不可行的,因爲不能訪問this。再思考一下,既然不能用this,那我用vuex的store不就行了?於是有了第一個方案。示意代碼如下:

export default {
    async asyncData({ store }) {
        const isLoaded = store.getters.isLoaded;
        if (isLoaded) return;
        const data = await axios.get('/foo/bar');
        store.dispatch('updateIsLoaded', true);
        return data;
  }
};

但是因爲我不喜歡用vuex,第一個是引入一個外部庫會影響性能(在我的場景下不需要vuex),第二個是會增加和外部的耦合。所以有沒有更好的方式呢?

如果脫離組件層面,比如在組件外部設置一個變量,然後緩存在組件外部,應該也可以;於是有了第二個方案。示意代碼如下:

const cache = { data: [], cached: false };

export default {
    async asyncData() {
        if (!cache.cached) {
            const data = await axios.get('/foo/bar');
            cache.data = data;
            cache.cached = true;
        }
        return cache.data;
    }
}

但是這個方案有一個問題,如果數據量很大,可能會導致內存泄漏。因爲keep-alive會緩存整個組件,組件內部又持有對外部的cache變量的引用,導致瀏覽器無法對cache進行GC,可能會導致內存泄漏。所以,在大數據量和移動端的場景下,可能會需要考慮這個問題,尤其是有些稍微舊一點的手機運行起來可用內存就還剩幾百M了,容易導致卡頓。所以,在對內存要求比較高的場景下,採用方案一會更好,因爲不需要重複進行緩存。

當然,也可以進行一些折中。比如,可以在keep-alive的組件deactivated的時候設置一個定時器,超過一定時間之後就清除cache(activated的時候移除定時器):

const cache = {
    data: [],
    cached: false,
    expires: 300 * 1000 // 300s過期
};
let cacheTimer;

export default {
    // ...
    activated() {
        clearTimeout(cacheTimer);
    },
    deactivated() {
        cacheTimer = setTimeout(() => {
            cache.data = [];
            cache.cached = false;
        }, cache.expires);
    }
}

或者,可以維護一個隊列或者棧,採用LRU之類的算法進行手動釋放,我覺得都是可行的折中處理。

總而言之,方案一的內存消耗較小,但是需要引入vuex,會增加加載時間(雖然gzip之後的vuex只有幾K,但是蚊子腿也是肉,而且如果用CDN,CDN掛了呢?),並且會增加和外部的耦合,增加複雜度;方案二的內存消耗較大,但是不需要引入額外的庫,耦合也比較小,內聚比較好。

這是我目前的方案,只是起一個拋磚引玉的作用。如果有更好的方案,還請不吝賜教。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章