Vue3源码解析--<keep-alive>的魔法

1,169 阅读4分钟

keep-alive组件

<keep-alive>是Vue.js的一个内置组件,可以使被包含的组件保留状态或避免重新渲染。在源码runtime-core/src/components/KeepAlive.ts中,下面来分析其实现原理。

setup方法中,会创建一个缓存容器和缓存的key列表,其代码如下所示:

setup(){
  /* 缓存对象 */
  const cache: Cache = new Map()
  const keys: Keys = new Set()
  // keep-alive组件的上下文对象
  const instance = getCurrentInstance()!
  const sharedContext = instance.ctx as KeepAliveContext
  // 替换内容
  sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
    const instance = vnode.component!
    move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
    // 处理props改变
    patch(
    ...
    )
    ...
  }
  // 替换内容
  sharedContext.deactivate = (vnode: VNode) => {
    const instance = vnode.component!
    move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
    ...
  }
}

<keep-alive>自己实现了render方法,并没有使用Vue内置的render方法(经过<template>内容提取,转换AST数,render字符串等一系列过程),在执行<keep-alive> 组件渲染时,就会执行这个render方法:

render () {
   // 得到slot插槽中的第一个组件
  const children = slots.default()
  const rawVNode = children[0]

  ...
  // 获取组件名称,优先获取组件的name字段
  const name = getComponentName(
    isAsyncWrapper(vnode)
      ? (vnode.type as ComponentOptions).__asyncResolved || {}
      : comp
  )
  // name不在include中或者exclude中,则直接返回vnode(没有存取缓存)
  const { include, exclude, max } = props

  if (
    (include && (!name || !matches(include, name))) ||
    (exclude && name && matches(exclude, name))
  ) {
    current = vnode
    return rawVNode
  }
  ...
  const key = vnode.key == null ? comp : vnode.key
  const cachedVNode = cache.get(key)

  // 如果已经缓存了,则直接从缓存中获取组件实例给vnode,若还未缓存则先进行缓存
  if (cachedVNode) {
    // copy over mounted state
    vnode.el = cachedVNode.el
    vnode.component = cachedVNode.component
    if (vnode.transition) {
      // recursively update transition hooks on subTree
      setTransitionHooks(vnode, vnode.transition!)
    }
    // avoid vnode being mounted as fresh
    vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
    // make this key the freshest
    keys.delete(key)
    keys.add(key)
  } else {
    keys.add(key)
    // prune oldest entry
    if (max && keys.size > parseInt(max as string, 10)) {
      pruneCacheEntry(keys.values().next().value)
    }
  }

  return rawVNode
}

render方法中,<keep-alive>缓存的并不是直接的DOM节点,而是Vue中内置的VNode对象,VNode经过render方法后,会被替换成真正的DOM内容。首先通过slots.default().children[0]获取第一个子组件,获取该组件的name。接下来会将这个name通过includeexclude属性进行匹配,匹配不成功(说明不需要进行缓存),则不进行任何操作直接返回VNode。需要注意的是,<keep-alive>只会处理它的第一个子组件,所以如果给<keep-alive>设置多个子组件,是无法生效的。

<keep-alive>还有一个watch方法,用来监听include以及exclude的改变,如下代码所示:

watch(
  () => [props.include, props.exclude],
  // 监视include以及exclude,在被修改时对cache进行修正
  ([include, exclude]) => {
    include && pruneCache(name => matches(include, name))
    exclude && pruneCache(name => !matches(exclude, name))
  },
  // prune post-render after `current` has been updated
  { flush: 'post', deep: true }
)

这里的程序逻辑是动态监听includeexclude的改变,从而动态地维护之前创建的缓存对象cache,其实就是对cache进行遍历,发现缓存的节点名称和新的规则没有匹配上时,就把这个缓存节点从缓存中摘除。下面来看看pruneCache这个方法,如下代码所示:

function pruneCache(filter?: (name: string) => boolean) {
  cache.forEach((vnode, key) => {
    const name = getComponentName(vnode.type as ConcreteComponent)
    if (name && (!filter || !filter(name))) {
      pruneCacheEntry(key)
    }
  })
}

遍历cache中的所有项,如果不符合filter指定的规则,则会执行pruneCacheEntry,如下代码所示:

function pruneCacheEntry(key: CacheKey) {
  const cached = cache.get(key) as VNode
  if (!current || cached.type !== current.type) {
    unmount(cached)
  } else if (current) {
    // current active instance should no longer be kept-alive.
    // we can't unmount it now but it might be later, so reset its flag now.
    resetShapeFlag(current)
  }
  // 销毁VNode对应的组件实例
  cache.delete(key)
  keys.delete(key)
}

上面内容完成以后,当响应式触发时,<keep-alive>里面的内容会改变,会调用<keep-alive>render方法得到VNode,这里并没有走很深层次的diff去对比缓存前后的VNode,而是直接将旧节点置为null,新节点进行替换,在patch方法中,直接命中这里的逻辑,如下代码所示:

// n1为缓存前的节点,n2为将要替换的节点
if (n1 && !isSameVNodeType(n1, n2)) {
  anchor = getNextHostNode(n1)
  // 卸载旧节点
  unmount(n1, parentComponent, parentSuspense, true)
  n1 = null
}

然后会经过setup方法中的sharedContext.activatesharedContext.deactivate来进行内容的替换,其核心是move方法,其代码如下所示:

const move: MoveFn = () => {
   // 替换DOM
   ...
   hostInsert(el!, container, anchor) // insertBefore修改DOM
}

总结一下,<keep-alive>组件也是一个Vue组件,它的实现是通过自定义的render方法并且使用了插槽。由于是直接使用VNode方式进行内容替换,不是直接存储DOM结构,所以不会执行组件内的生命周期方法,它通过includeexclude维护组件的cache对象,从而来处理缓存中的具体逻辑。