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,这里我们就需要思考什么可以作为每个页面的标记

两种方式:

  1. 通过每个url的查询参数来存储key
  2. 通过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核心逻辑阐述完毕。

参考文档

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Vue3中的keep-alive指令可以用来缓存页面,以便在切换回该页面时,能够快速加载并保留之前的状态。 要使用keep-alive,需要将其放置在需要缓存的组件外部,并将要缓存的组件放置在keep-alive的子元素中。例如: ``` <template> <keep-alive> <component v-bind:is="currentComponent" /> </keep-alive> </template> ``` 在这个例子中,keep-alive缓存currentComponent指定的组件,并在下一次需要显示该组件时,直接从缓存中获取。 同时,keep-alive还提供了activated和deactivated两个生命周期钩子函数,可以用来在组件被缓存或激活时执行一些操作。例如: ``` <template> <keep-alive v-on:activated="onActivated" v-on:deactivated="onDeactivated"> <component v-bind:is="currentComponent" /> </keep-alive> </template> <script> export default { data() { return { currentComponent: 'MyComponent' } }, methods: { onActivated() { console.log('组件被激活') }, onDeactivated() { console.log('组件被缓存') } } } </script> ``` 在这个例子中,当MyComponent被激活或缓存时,分别会调用onActivated和onDeactivated函数。 ### 回答2: Vue3中的keep-alive组件是用来缓存某个页面的状态,以便下次进入时可以直接使用缓存的状态,提高页面的加载效率。相比于Vue2,Vue3的keep-alive组件有着一些新的特性和改动。 一、使用方式 Vue3中的keep-alive组件可以直接在路由配置中使用,例如: ``` const routes = [ { path: '/', component: Home, meta: { keepAlive: true } } ] ``` 这里我们通过设置meta信息来控制是否开启缓存,如果设置了keepAlive为true,则开启缓存。 另外,我们还可以通过编程式路由来动态控制keep-alive缓存状态: ``` <keep-alive :include="include" :exclude="exclude"> <router-view /> </keep-alive> ``` 这里我们可以通过include和exclude属性来控制keep-alive缓存的组件,当组件符合include规则时,会被缓存,否则会被排除在缓存外。 二、新特性 在Vue3中,keep-alive组件引入了新的生命周期钩子函数,包括: - onActivate:在缓存命中并且组件被激活时调用; - onDeactivate:在组件离开缓存时调用。 我们可以通过在keep-alive内部的组件中实现这两个生命周期函数来控制缓存时的行为,例如: ``` <keep-alive> <router-view v-slot="{ Component }"> <transition name="fade"> <component :is="Component" /> </transition> </router-view> </keep-alive> <script> export default { onActivate() { // 缓存命中并且组件被激活时调用 console.log('缓存命中并且组件被激活'); }, onDeactivate() { // 组件离开缓存时调用 console.log('组件离开缓存'); } }; </script> ``` 三、注意事项 在使用keep-alive组件时需要注意以下几点: - 如果页面数据会不断变化,那么开启缓存可能会导致状态不同步,需要通过onActivate和onDeactivate函数来控制缓存的更新; - 缓存大量数据可能会导致页面卡顿,需要根据页面的特点和数据量来决定是否开启缓存; - 缓存的内容随时可能被销毁,需要考虑如何保存数据以防下次进入时数据丢失。 以上就是关于Vue3中keep-alive组件的说明,在正确使用的情况下,keep-alive组件可以提高页面的性能和用户体验。 ### 回答3: Vue.js是一个流行的JavaScript前端框架,它提供了一个名为Vue的对象,允许我们创建复杂的UI界面。Vue.js提供了一个名为keep-alive的组件,它可以帮助我们缓存页面,以便在页面切换时不必重新加载页面Vue.js keep-alive组件的工作原理是,它将当前活动组件缓存起来,当用户再次访问此组件时,它会从缓存中获取并重新渲染该组件,而不是重新创建它。这使得访问同一页面时的响应速度快了很多。 使用Vue.js keep-alive组件,我们需要将组件包含在keep-alive标签中。例如: ``` <template> <div> <keep-alive> <component :is="currentComponent"></component> </keep-alive> </div> </template> <script> export default { data() { return { currentComponent: 'Component1' } } } </script> ``` 在这个示例中,我们将一个组件包含在keep-alive标签中,并使用currentComponent变量来指定当前组件。这样做可以确保在同一页面上切换组件时不会重新加载组件。只有在第一次访问组件时,组件才会被创建和渲染。 除了包含组件在keep-alive标签中外,我们还可以使用activated和deactivated钩子来实现进入和离开缓存时的一些操作。例如,在进入缓存时,我们可以利用activated钩子重新加载数据来更新组件。在离开缓存时,我们可以利用deactivated钩子保存组件状态。 总之,Vue.js keep-alive组件可以帮助我们提高页面响应速度,特别是在访问同一页面时。使用keep-alive组件,我们可以缓存活动组件并在需要时重新渲染,而不必重新创建它们。同时,我们还可以利用activated和deactivated钩子来实现更多的操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值