# 在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 />
就行了,还是稍微有点粗暴了。
在此之前,我考虑过这样几种处理方式:
- 在
asyncData
里使用缓存。但是,后来发现这种方案不太可行,因为asyncData
在服务端触发,无法调用this
,这意味着数据是无法缓存在组件里的,比如拿到数据之后放在data
里,然后每次都从data
里读数据。 - 使用
mounted
和activated
钩子,按照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挂了呢?),并且会增加和外部的耦合,增加复杂度;方案二的内存消耗较大,但是不需要引入额外的库,耦合也比较小,内聚比较好。
这是我目前的方案,只是起一个抛砖引玉的作用。如果有更好的方案,还请不吝赐教。