KeepAlive 用来缓存组件实例,让组件在切换时不会被重复创建和销毁。

2025-11-23 16:04:26

KeepAlive 用来缓存组件实例,让组件在切换时不会被重复创建和销毁。

常用于动态组件、路由页面切换等场景:组件离开页面时先移动到缓存容器中,再次渲染时直接复用之前的组件实例和真实 DOM。

这样可以保留组件内部状态,比如用户输入、滚动位置等,也可以减少不必要的重新挂载。

<script type="module">
  import {
    h,
    createApp,
    KeepAlive,
    ref,
    onMounted,
    onUnmounted,
  } from '../dist/vue.esm.js'

  const Comp1 = {
    name: 'Com1',
    setup() {
      onMounted(() => {
        console.log('Com1 mounted')
      })

      onUnmounted(() => {
        console.log('Com1 unmount')
      })

      return () => h('div', ['Com1', h('input')])
    },
  }

  const Comp2 = {
    name: 'Com2',
    setup() {
      onMounted(() => {
        console.log('Com2 mounted')
      })

      onUnmounted(() => {
        console.log('Com2 unmount')
      })

      return () => h('div', ['Com2', h('input')])
    },
  }

  const Comp3 = {
    name: 'Com3',
    setup() {
      onMounted(() => {
        console.log('Com3 mounted')
      })

      onUnmounted(() => {
        console.log('Com3 unmount')
      })

      return () => h('div', ['Com3', h('input')])
    },
  }

  const App = {
    name: 'App',
    setup() {
      const components = [Comp1, Comp2, Comp3]
      const index = ref(0)

      return () => {
        return h('div', [
          h('button', {
            onClick() {
              index.value = index.value === 2 ? 0 : index.value + 1
            },
          }, 'change'),
          h(KeepAlive, null, () => h(components[index.value])),
        ])
      }
    },
  }

  createApp(App).mount('#app')
</script>

第一次切换到某个组件时,会正常触发 mounted。

再次切换回来时,不会重新创建组件实例,也不会再次触发 mounted。原来输入框里的内容也会保留下来。

渲染流程

实现主要分为四步:

  1. KeepAlive 渲染默认插槽中的组件,并用 Map 缓存组件 vnode
  2. 组件被切走时,不执行真正的卸载,而是移动到隐藏容器中
  3. 组件再次渲染时,复用缓存中的组件实例和真实 DOM
  4. processComponent 根据标记决定是挂载新组件,还是激活缓存组件

实现

1. 基础结构

先定义一个最简单的 KeepAlive:

export const isKeepAlive = type => type.__isKeepAlive

export const KeepAlive = {
  name: 'KeepAlive',
  __isKeepAlive: true,
  setup(props, { slots }) {
    return () => {
      return slots.default()
    }
  }
}

这样已经可以正常渲染插槽中的组件。

但现在只是把组件原样返回,还没有做缓存。组件切换时,旧组件仍然会走正常的卸载流程。

2. 缓存组件 vnode

export const KeepAlive = {
  name: 'KeepAlive',
  __isKeepAlive: true,
  setup(props, { slots }) {
    const cache = new Map()

    return () => {
      const vnode = slots.default()
      const key = vnode.key ?? vnode.type

      cache.set(key, vnode)

      return vnode
    }
  }
}

cache 的作用是保存已经渲染过的组件 vnode。

但只缓存 vnode 在组件切换时,旧组件会被 unmount 删除,真实 DOM 和组件实例都会丢失。

所以再给 vnode 打一个标记,告诉渲染器:这个组件需要被 KeepAlive 缓存,不要真的卸载。

return () => {
  const vnode = slots.default()
  const key = vnode.key ?? vnode.type

  cache.set(key, vnode)

  vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

  return vnode
}

COMPONENT_SHOULD_KEEP_ALIVE 表示组件离开页面时应该进入缓存,而不是直接销毁。

3. 暂停卸载

卸载逻辑在渲染器的 unmount 也要识别 COMPONENT_SHOULD_KEEP_ALIVE:

function unmount(vnode) {
  const { shapeFlag, children, ref } = vnode

  if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
    const parentComponent = vnode.component.parent
    parentComponent.ctx.deactivate(vnode)
    return
  }

  if (shapeFlag & ShapeFlags.COMPONENT) {
    unmountComponent(vnode.component)
  }
  else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    unmountChildren(children)
  }

  hostRemove(vnode.el)

  if (ref != null) {
    setRef(ref, null)
  }
}

如果组件需要被缓存,就不再继续执行 unmountComponent 和 hostRemove。

但只是跳过卸载还不够。旧组件的真实 DOM 仍然停留在页面中,所以需要把它移动到一个隐藏容器里。

移动逻辑在 KeepAlive 的 deactivate 中:

export const KeepAlive = {
  name: 'KeepAlive',
  __isKeepAlive: true,
  setup(props, { slots }) {
    const cache = new Map()
    const instance = getCurrentInstance()
    const { options } = instance.ctx.renderer
    const { createElement, insert } = options
    const storageContainer = createElement('div')

    instance.ctx.deactivate = (vnode) => {
      insert(vnode.el, storageContainer)
    }

    return () => {
      const vnode = slots.default()
      const key = vnode.key ?? vnode.type

      cache.set(key, vnode)
      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

      return vnode
    }
  }
}

storageContainer 不是挂载到页面上,只是用来临时保存被缓存组件的真实 DOM。

这样组件切走时,组件实例不会销毁,真实 DOM 也只是从当前容器移动到了缓存容器。

4. 传入渲染器能力

KeepAlive 需要调用平台相关的 insert 和 createElement。

这些方法属于 runtime-dom,所以在挂载 KeepAlive 组件时,需要把渲染器能力传给组件实例:

function mountComponent(vnode, container, anchor, parentComponent) {
  const instance = createComponentInstance(vnode, parentComponent)

  if (isKeepAlive(vnode.type)) {
    instance.ctx.renderer = { options }
  }

  vnode.component = instance

  setupComponent(instance)
  setupRenderEffect(instance, container, anchor)
}

这样 KeepAlive 内部就可以通过 instance.ctx.renderer.options 拿到平台操作方法。

5. 使用缓存

再次渲染同一个组件时,如果缓存中已经存在对应的 vnode,就复用之前的组件实例和真实 DOM:

export const KeepAlive = {
  name: 'KeepAlive',
  __isKeepAlive: true,
  setup(props, { slots }) {
    const cache = new Map()
    const instance = getCurrentInstance()
    const { options } = instance.ctx.renderer
    const { createElement, insert } = options
    const storageContainer = createElement('div')

    instance.ctx.activate = (vnode, container, anchor) => {
      insert(vnode.el, container, anchor)
    }

    instance.ctx.deactivate = (vnode) => {
      insert(vnode.el, storageContainer)
    }

    return () => {
      const vnode = slots.default()
      const key = vnode.key ?? vnode.type
      const cachedVNode = cache.get(key)

      if (cachedVNode) {
        vnode.component = cachedVNode.component
        vnode.el = cachedVNode.el
        vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
      }

      cache.set(key, vnode)
      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

      return vnode
    }
  }
}

这里有两个标记:

  • COMPONENT_SHOULD_KEEP_ALIVE:当前组件以后被卸载时,需要进入缓存
  • COMPONENT_KEPT_ALIVE:当前组件已经命中过缓存,本次渲染需要复用

命中缓存时,需要把旧 vnode 上的 component 和 el 复制到新 vnode 上。

这样后续渲染器就能继续使用之前的组件实例和真实 DOM。

6. 激活缓存组件

最后在 processComponent 中处理 COMPONENT_KEPT_ALIVE:

function processComponent(n1, n2, container, anchor, parentComponent) {
  if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
    parentComponent.ctx.activate(n2, container, anchor)
    return
  }

  if (n1 == null) {
    mountComponent(n2, container, anchor, parentComponent)
  }
  else {
    updateComponent(n1, n2)
  }
}

如果组件已经被缓存过,就不再调用 mountComponent。

此时只需要调用父组件上的 activate,把缓存容器中的真实 DOM 移回当前容器。