# 在nuxt中使用keep-alive的两种可能方案

关于Vue中keep-alive的作用,以及在部分场景下由于避免重复请求、重复渲染的性能提升,想必大家都很清楚了,在此不再赘述。

但是在nuxt项目中使用keep-alive就有个问题,比如我有一个用到嵌套路由的页面,导航栏(或者标签页)由几个页面共享,然后根据路由切换页面内容。这应该是个很常见的场景,Vue的文档里keep-alive的部分用的也是这个例子。为了直观,放个图好了:

1.png

在这个场景下使用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挂了呢?),并且会增加和外部的耦合,增加复杂度;方案二的内存消耗较大,但是不需要引入额外的库,耦合也比较小,内聚比较好。

这是我目前的方案,只是起一个抛砖引玉的作用。如果有更好的方案,还请不吝赐教。

最后更新于: 6/25/2020, 2:10:06 PM