在剖析feb-alive实现以前,但愿你们对如下基本知识有必定的了解。vue
keep-alive配合vue-router在动态路由切换的状况下不会触发activated钩子
,由于切换的时候组件没有变化,因此只能经过beforeRouteUpdate
钩子或者监听$route
来实现数据更新,而feb-alive在动态路由切换时,依然会触发activated钩子,因此用户能够放心的将业务更新逻辑写在activated钩子,没必要关心动态路由仍是非动态路由的状况。html5
因此在上文中讲到的使用keep-alive存在的一些限制问题都可以获得有效的解决node
首先咱们的目标很明确,须要开发的是一个页面级别
的缓存插件,以前使用keep-alive遇到的诸多问题,归根结底是由于它是一个组件级别
的缓存。那么咱们就须要寻找每一个页面的特征,用来存储咱们须要存储的路由组件vnode,这里咱们就须要思考什么能够做为每一个页面的标记git
两种方式:github
方案一:使用查询参数vue-router
优势:api
缺点:浏览器
方案二:使用history.state缓存
优势:
缺点:
相比方案一明显的缺点,我更较倾向于方案二,舍弃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所在层级,这是为了解决嵌套路由的使用。
继续分析代码
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逻辑
/foo -> /bar
复制代码
/page/1 -> /page/2
复制代码
/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核心逻辑阐述完毕。