浅谈Vue页面级缓存解决方案feb-alive (下)
feb-alive
Vue页面级缓存解决方案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核心逻辑阐述完毕。
参考文档
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。