keep-alive实现原理及Vue2/Vue3对比分析

一、keep-alive基本概念

keep-alive是Vue的内置组件,用于缓存组件实例,避免重复渲染。它具有以下特点:

  1. 抽象组件:自身不会渲染DOM,也不会出现在父组件链中
  2. 包裹动态组件:缓存不活动的组件实例,而不是销毁它们
  3. 生命周期:提供activated和deactivated钩子函数

二、keep-alive核心实现原理

1. 基本工作流程

  1. 判断当前组件是否需要缓存
  2. 生成组件唯一key
  3. 缓存组件实例
  4. 在被包裹组件上触发对应的生命周期钩子

2. 缓存策略

采用LRU(Least Recently Used)算法:

  • 设置最大缓存数量(max属性)
  • 优先删除最久未使用的组件
  • 新组件加入时,若达到上限则删除最旧组件

三、Vue2实现原理

js 复制代码
// Vue2 中 keep-alive 的核心实现
export default {
  name: 'keep-alive',
  abstract: true, // 抽象组件标识
  
  props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: [String, Number]
  },

  created () {
    this.cache = Object.create(null) // 缓存对象
    this.keys = [] // 缓存key数组
  },

  destroyed () {
    // 销毁所有缓存实例
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key)
    }
  },

  mounted () {
    // 监听include和exclude的变化
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render () {
    const slot = this.$slots.default
    const vnode = getFirstComponentChild(slot)
    const componentOptions = vnode && vnode.componentOptions
    
    if (componentOptions) {
      const name = getComponentName(componentOptions)
      const { include, exclude } = this
      
      // 判断是否需要缓存
      if (
        (include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      const key = vnode.key == null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      
      // 命中缓存
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        remove(keys, key)
        keys.push(key) // 调整key顺序
      } else {
        cache[key] = vnode // 缓存组件
        keys.push(key)
        // 超过max限制时清理最久未使用的组件
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0])
          keys.shift()
        }
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}

四、Vue3实现原理

js 复制代码
// Vue3 中 keep-alive 的核心实现
export const KeepAliveImpl = {
  name: 'KeepAlive',
  __isKeepAlive: true,

  props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: [String, Number]
  },

  setup(props, { slots }) {
    const cache = new Map() // 使用Map存储缓存
    const keys = new Set() // 使用Set存储keys
    const current = getCurrentInstance()
    
    // 缓存子树
    const cacheSubtree = () => {
      if (current.subTree) {
        cache.set(current.subTree.key, current.subTree)
        keys.add(current.subTree.key)
      }
    }

    // 修剪缓存
    const pruneCache = (filter?: (name: string) => boolean) => {
      cache.forEach((vnode, key) => {
        const name = vnode.type.name
        if (name && (!filter || filter(name))) {
          pruneCacheEntry(key)
        }
      })
    }

    // 清理缓存条目
    const pruneCacheEntry = (key: CacheKey) => {
      const cached = cache.get(key)
      if (!current || !isSameVNodeType(cached, current)) {
        unmount(cached)
      }
      cache.delete(key)
      keys.delete(key)
    }

    // 监听include/exclude变化
    watch(
      () => [props.include, props.exclude],
      ([include, exclude]) => {
        include && pruneCache(name => matches(include, name))
        exclude && pruneCache(name => !matches(exclude, name))
      }
    )

    // 卸载时清理所有缓存
    onBeforeUnmount(() => {
      cache.forEach(cached => {
        unmount(cached)
      })
    })

    return () => {
      const children = slots.default?.()
      if (!children) return null
      
      const vnode = children[0]
      if (!vnode) return null
      
      const comp = vnode.type
      const name = comp.name
      
      // 检查是否应该缓存
      if (
        (props.include && (!name || !matches(props.include, name))) ||
        (props.exclude && name && matches(props.exclude, name))
      ) {
        return vnode
      }

      const key = vnode.key == null ? comp : vnode.key
      const cached = cache.get(key)

      // 命中缓存
      if (cached) {
        vnode.el = cached.el
        vnode.component = cached.component
        // 标记为kept-alive
        vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
      } else {
        // 缓存新组件
        cache.set(key, vnode)
        keys.add(key)
        // 超过max限制时清理
        if (props.max && cache.size > parseInt(props.max)) {
          pruneCacheEntry(keys.values().next().value)
        }
      }
      
      // 标记keepAlive
      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
      return vnode
    }
  }
}

五、Vue2和Vue3实现差异对比

1. 数据结构

  • Vue2: 使用普通对象和数组存储缓存

    js 复制代码
    this.cache = Object.create(null)
    this.keys = []
  • Vue3: 使用Map和Set存储缓存

    js 复制代码
    const cache = new Map()
    const keys = new Set()

2. 组件实现方式

  • Vue2: 选项式API,通过created、mounted等生命周期实现
  • Vue3: 组合式API,使用setup函数实现,逻辑更集中

3. 渲染机制

  • Vue2: 在render函数中直接操作VNode
  • Vue3: 使用新的渲染器架构,更好地支持Fragment和异步组件

4. 性能优化

  • Vue3优势:
    1. 更高效的响应式系统
    2. 更智能的编译优化
    3. 更好的Tree-shaking支持
    4. 更完善的TypeScript支持

5. 生命周期钩子

  • Vue2:

    js 复制代码
    export default {
      activated() {},
      deactivated() {}
    }
  • Vue3:

    js 复制代码
    import { onActivated, onDeactivated } from 'vue'
    
    setup() {
      onActivated(() => {})
      onDeactivated(() => {})
    }

六、使用方法案例

1. Vue2中的使用方法

基础用法
html 复制代码
<!-- App.vue -->
<template>
  <div id="app">
    <keep-alive>
      <component :is="currentComponent"></component>
    </keep-alive>
  </div>
</template>

<script>
import ComponentA from './components/ComponentA.vue'
import ComponentB from './components/ComponentB.vue'

export default {
  name: 'App',
  components: {
    ComponentA,
    ComponentB
  },
  data() {
    return {
      currentComponent: 'ComponentA'
    }
  }
}
</script>
配合路由使用
js 复制代码
// router.js
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/list',
    component: () => import('./views/List.vue'),
    meta: {
      keepAlive: true // 需要缓存的路由
    }
  },
  {
    path: '/detail',
    component: () => import('./views/Detail.vue'),
    meta: {
      keepAlive: false // 不需要缓存的路由
    }
  }
]

export default new VueRouter({
  routes
})
html 复制代码
<!-- App.vue -->
<template>
  <div id="app">
    <!-- 缓存路由组件 -->
    <keep-alive>
      <router-view v-if="$route.meta.keepAlive"></router-view>
    </keep-alive>
    <!-- 不缓存的路由组件 -->
    <router-view v-if="!$route.meta.keepAlive"></router-view>
  </div>
</template>
使用include和exclude
html 复制代码
<template>
  <div id="app">
    <keep-alive :include="['ComponentA', 'ComponentB']" :exclude="['ComponentC']">
      <router-view></router-view>
    </keep-alive>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

2. Vue3中的使用方法

基础用法
html 复制代码
<!-- App.vue -->
<template>
  <div id="app">
    <KeepAlive>
      <component :is="currentComponent"></component>
    </KeepAlive>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import ComponentA from './components/ComponentA.vue'
import ComponentB from './components/ComponentB.vue'

const currentComponent = ref('ComponentA')
</script>
配合路由使用
ts 复制代码
// router.ts
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/list',
    component: () => import('./views/List.vue'),
    meta: {
      keepAlive: true
    }
  },
  {
    path: '/detail',
    component: () => import('./views/Detail.vue'),
    meta: {
      keepAlive: false
    }
  }
]

export default createRouter({
  history: createWebHistory(),
  routes
})
html 复制代码
<!-- App.vue -->
<template>
  <div id="app">
    <RouterView v-slot="{ Component }">
      <KeepAlive>
        <component :is="Component" v-if="$route.meta.keepAlive" />
      </KeepAlive>
      <component :is="Component" v-if="!$route.meta.keepAlive" />
    </RouterView>
  </div>
</template>

<script setup>
import { RouterView } from 'vue-router'
</script>
使用include和exclude
html 复制代码
<!-- App.vue -->
<template>
  <div id="app">
    <RouterView v-slot="{ Component }">
      <KeepAlive :include="['ListPage']" :max="10">
        <component :is="Component" />
      </KeepAlive>
    </RouterView>
  </div>
</template>

<script setup>
import { RouterView } from 'vue-router'
</script>
在组件中使用生命周期钩子
html 复制代码
<!-- ListPage.vue -->
<template>
  <div class="list-page">
    <ul>
      <li v-for="item in list" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script setup>
import { ref, onActivated, onDeactivated } from 'vue'

const list = ref([])

// 在组件被激活时触发
onActivated(() => {
  console.log('组件被激活')
  // 可以在这里恢复组件的状态,如滚动位置
})

// 在组件被停用时触发
onDeactivated(() => {
  console.log('组件被停用')
  // 可以在这里保存组件的状态
})
</script>

七、总结

  1. 核心原理相同:

    • 都使用LRU缓存策略
    • 都支持include/exclude过滤
    • 都实现了组件缓存和重用
  2. 主要改进:

    • Vue3使用更现代的数据结构
    • 更清晰的代码组织方式
    • 更好的性能优化
    • 更强大的TypeScript支持
    • 更完善的错误处理机制
相关推荐
sirius星夜10 分钟前
鸿蒙开发实践:深入使用 AppGallery Connect 提升应用开发效率
javascript
InlaidHarp13 分钟前
Elpis DSL领域模型设计理念
前端
lichenyang45315 分钟前
react-route-dom@6
前端
番茄比较犟17 分钟前
widget的同级移动
前端
每天吃饭的羊21 分钟前
面试题-函数入参为interface类型进行约束
前端
屋外雨大,惊蛰出没1 小时前
Vue+spring boot前后端分离项目搭建---小白入门
前端·vue.js·spring boot
梦语花1 小时前
如何在前端项目中优雅地实现异步请求重试机制
前端
sirius星夜1 小时前
鸿蒙功效:"AbilitySlice"的远程启动和参数传递
javascript
彬师傅1 小时前
JSAPITHREE-自定义瓦片服务加载
前端·javascript
梦语花1 小时前
深入探讨前端本地存储方案:Dexie.js 与其他存储方式的对比
javascript