剑大瑞

V1

2022/01/26阅读:55主题:全栈蓝

KeepAlive 使用及原理分析

KeepAlive 使用及原理分析

KeepAlive组件是Vue中的内置组件,主要用于保留组件状态或者避免组件重新渲染。

KeepAlive组件接受三个Props属性:

  • include - string | RegExp | Array。只有名称匹配的组件会被缓存。
  • exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存。
  • max - number | string。最多可以缓存多少组件实例。

使用方法:

<!-- 基本 -->
<keep-alive>
  <component :is="view"></component>
</keep-alive>

<!-- 多个条件判断的子组件 -->
<keep-alive>
  <comp-a v-if="a > 1"></comp-a>
  <comp-b v-else></comp-b>
</keep-alive>

<!-- 和 `<transition>` 一起使用 -->
<transition>
  <keep-alive>
    <component :is="view"></component>
  </keep-alive>
</transition>

<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<!-- regex (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<!-- Array (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

<!-- max属性设置缓存上限 -->
<keep-alive :max="10">
  <component :is="view"></component>
</keep-alive>

上面代码简单介绍了KeepAlive的使用方式,下面我们带着问题出发,从问题去分析KeepAlive组件的原理。

KeepAlive返回的是什么?

看下简略版的代码:

const KeepAliveImpl = {
  name`KeepAlive`,

  // 私有属性 标记 该组件是一个KeepAlive组件
  __isKeepAlivetrue,
  props: {
    // 用于匹配需要缓存的组件
    include: [StringRegExpArray],
    // 用于匹配不需要缓存的组件
    exclude: [StringRegExpArray],
    // 用于设置缓存上线
    max: [StringNumber]
  },
  setup(props, { slots }) {
    // 省略部分代码...

    // 返回一个函数
    return () => {
      if (!slots.default) {
        return null
      }
      
      // 省略部分代码...
      
      // 获取子节点
      const children = slots.default()
      // 获取第一个子节点
      const rawVNode = children[0]
      // 返回原始Vnode
      return rawVNode
    }
  }
}

通过上面的代码可以知道,KeepAlive组件是一个抽象组件

组件中并没有我们经常使用的模板template或者返回一个render函数。

setup函数中,通过参数slots.default()获取到KeepAlive组件包裹的子组件列表。

最终返回的是第一个子组件rawVnode。且仅支持缓存第一个子节点。

细心的同学,可能注意,我们平时使用setup函数时,最终返回的结果是一个对象。

KeepAlive返回的是一个箭头函数。这里关于setup返回函数的分析,我们会在后续的文章中进行学习。

KeepAlive是如何进行组件筛选的?

在使用KeepAlive时,我们可以通过配置include & exclude属性来实现对目标组件的缓存。include & exclude 属性可以配置stringarrayregExp类型。下面一起看下KeepAlive是怎么利用这两个属性进行组件筛选的。

const KeepAliveImpl = {
  setup(props, { slots }) {
    // 1️⃣ 缓存Vnode
    const cache: Cache = new Map()
    // 记录被缓存Vnode的key
    const keys: Keys = new Set()

    // 2️⃣ 修剪缓存
    function pruneCache(filter{
      cache.forEach((vnode, key) => {
        // 获取组件名称
        const name = getComponentName(vnode.type) 
        if (name && (!filter || !filter(name))) {
          pruneCacheEntry(key)
        }
      })
    }
    function pruneCacheEntry(key{
      // 省略部分代码...
      cache.delete(key)
      keys.delete(key)
    }

    // prune cache on include/exclude prop change
    // 3️⃣ 侦测筛选条件,当include/exclude发生变化的时候,更新缓存
    watch(
      () => [props.include, props.exclude],
      ([include, exclude]) => {
        include && pruneCache(name => matches(include, name))
        exclude && pruneCache(name => !matches(exclude, name))
      },
      // prune post-render after `current` has been updated
      { flush'post'deeptrue }
    )

    return () => {
  
      // 省略部分代码...
      
      const children = slots.default()
      const rawVNode = children[0
      let vnode = getInnerChild(rawVNode)
      const comp = vnode.type

      // for async components, name check should be based in its loaded
      // inner component if available
      // 对于异步组件 名称校验应该基于被加载的组件
      const name = getComponentName(
        isAsyncWrapper(vnode)
          ? (vnode.type).__asyncResolved || {}
          : comp
      )
      const { include, exclude, max } = props
      
      // 4️⃣ 筛选Vnode
      if (
        (include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        current = vnode
        return rawVNode
      }

      const key = vnode.key == null ? comp : vnode.key
      // 从缓存中获取Vnode
      const cachedVNode = cache.get(key)

 
      if (cachedVNode) {
         // 省略部分代码...
      } else {
        // 如果先前没有缓存Vnode
        // 则直接添加
        keys.add(key)
        // prune oldest entry
        // 5️⃣ 删除最旧的
        if (max && keys.size > parseInt(max, 10)) {
          pruneCacheEntry(keys.values().next().value)
        }
      }

    }
  }
}

上面的代码我们可以分成五部分进行分析:

  1. 常量cache用于映射缓存组件的key : Vnode,常量keys用于记录已经被缓存的Vnodekey
  2. 负责修剪cachekeyspruneCachepruneCacheEntry方法。主要职责是通过遍历cache,执行filter函数,修剪cachekeys
  3. 负责侦测筛选条件的watch,当筛选条件发生变化的时候,会执行pruneCache,更新cachekeys。筛选条件就是我们传入的props中的includeexclude
  4. 用于筛选符合筛选条件的Vnode,不符合缓存条件的,会直接返回rawVnode,不会被cachekeys缓存。
  5. 用于判断是否已经超过缓存上限,如果超过,会删除最开始被缓存的Vnode

对于代码中的matches函数,是一个用于匹配的工具函数。

function matches(pattern, name{
  if (isArray(pattern)) {
    // 数组类型,递归matches
    return pattern.some((p) => matches(p, name))
  } else if (isString(pattern)) {
    return pattern.split(',').indexOf(name) > -1
  } else if (pattern.test) {
    return pattern.test(name)
  }
  /* istanbul ignore next */
  return false
}

在哪个阶段构建的缓存?

下面我们一起看下KeepAlive组件是本组件的哪个生命周期中进行的缓存构建:

const KeepAliveImpl = {
  setup(props, { slots }) {
    // 获取当前渲染实例
    const instance = getCurrentInstance()!
    const sharedContext = instance.ctx

    // if the internal renderer is not registered, it indicates that this is server-side rendering,
    // for KeepAlive, we just need to render its children
    // 服务端渲染的处理方式
    if (!sharedContext.renderer) {
      return slots.default
    }
    // 缓存
    const cache = new Map()
    const keys = new Set()
    let current = null

    // cache sub tree after render
    // 渲染之后缓存子节点
    let pendingCacheKey = null
    const cacheSubtree = () => {
      // fix #1621, the pendingCacheKey could be 0
      if (pendingCacheKey != null) {
        cache.set(pendingCacheKey, getInnerChild(instance.subTree))
      }
    }
    // 🟢 缓存组件
    onMounted(cacheSubtree)
    onUpdated(cacheSubtree)

    // 返回一个渲染函数
    return () => {
      // 省略部分代码
      pendingCacheKey = null

      // 获取内部子节点
      // keepAlive一般用户缓存路由组件包含的组件
      // 或者component包含的组件,这步操作就相当于获取路由组件或者动态组件包裹的子节点
      let vnode = getInnerChild(rawVNode)
      // 获取节点类型
      const comp = vnode.type
      
      const key = vnode.key == null ? comp : vnode.key
      // 从缓存中获取Vnode
      const cachedVNode = cache.get(key)

      // #1513 it's possible for the returned vnode to be cloned due to attr
      // fallthrough or scopeId, so the vnode here may not be the final vnode
      // that is mounted. Instead of caching it directly, we store the pending
      // key and cache `instance.subTree` (the normalized vnode) in
      // beforeMount/beforeUpdate hooks.
      
      // 这里更新pendingCacheKey是因为attr fallthrough 或者 scopeId变化需要返回一个经过克隆的Vnode,
      // 因此这里的Vnode并不能作为最终渲染所使用的的Vnode。
      // 不是直接缓存,而是在 beforeMount/beforeUpdate阶段
      // 存储pending状态的key和要缓存的Vnode。(翻译的不好,望指教~~~)
      pendingCacheKey = key
      
      if (cachedVNode) {
        // 省略部分代码...
        
        // make this key the freshest
        // 更新key
        keys.delete(key)
        keys.add(key)
      } else {
        // 如果先前没有缓存Vnode
        // 则直接添加
        keys.add(key)
        // prune oldest entry
        // 删除最旧的
        if (max && keys.size > parseInt(max as string, 10)) {
          pruneCacheEntry(keys.values().next().value)
        }
      }
      return rawVNode
    }
  }
}

通过上面的代码可以知道:

  • Vnodecache构建,是在KeepAlive组件的onMounted && onUpdated两个生命周期通过cacheSubtree方法构建的。
  • 变量pendingCacheKey主要用于记录处理pending状态的key
  • 如果组件的Vnode先前被Vnode被缓存过,在获取到cachedVNode之后,会更新keys中对应的key

activated & deactivate钩子函数实现

经过KeepAlive包裹组件,在切换时,它的生命周期钩子mountedunmouned生命周期钩子不会被调用,而是被缓存组件独有的两个生命周期钩子所代替:activateddeactivated。这两个钩子会被用于KeepAlive的直接子节点和所有子孙节点。

const KeepAliveImpl = {
  setup(props, { slots }) {
    // 获取当前渲染实例
    const instance = getCurrentInstance()!
    // KeepAlive communicates with the instantiated renderer via the
    // ctx where the renderer passes in its internals,
    // and the KeepAlive instance exposes activate/deactivate implementations.
    // The whole point of this is to avoid importing KeepAlive directly in the
    // renderer to facilitate tree-shaking.
    const sharedContext = instance.ctx


    let current: VNode | null = null

    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      ;(instance as any).__v_cache = cache
    }
    // 悬挂
    const parentSuspense = instance.suspense
    // 解构获取内部渲染器
    // 其实就是basecreaterender函数中的方法
    const {
      renderer: {
        p: patch,
        m: move,
        um: _unmount,
        o: { createElement }
      }
    } = sharedContext
    // 创建存储容器
    const storageContainer = createElement('div')
    
    // 🟢 存活时
    sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
      const instance = vnode.component!
      // 移动节点
      move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
      // in case props have changed
      // 某些情况下属性可能发生改变
      patch(
        instance.vnode,
        vnode,
        container,
        anchor,
        instance,
        parentSuspense,
        isSVG,
        vnode.slotScopeIds,
        optimized
      )
      // 后置任务池中 push 任务
      queuePostRenderEffect(() => {
        instance.isDeactivated = false
        if (instance.a) {
          invokeArrayFns(instance.a)
        }
        const vnodeHook = vnode.props && vnode.props.onVnodeMounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
      }, parentSuspense)
    }
    // 🟡 失活时
    sharedContext.deactivate = (vnode) => {
      const instance = vnode.component!
      move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
      
      queuePostRenderEffect(() => {
        if (instance.da) {
          invokeArrayFns(instance.da)
        }
        const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
        instance.isDeactivated = true
      }, parentSuspense)

    }
  }
}

从上面的代码可以知道:

  • 代码首先会从当前实例的上下文中获取渲染相关的方法,这些方法其实是在renderer中创建并配置好的,当patch组件时,会首先执行mountComponent方法,当组件是KeepAlive组件时,会绑定渲染相关的属性,因此在这里解构可以获取到mountpatchmove等方法。
  • activated方法主要负责移动节点、调用patch方法,向任务调度器中的后置任务池中push Vnode相关的钩子。
  • deactivated方法会通过move方法移除Vnode,向任务调度器中的后置任务池中push 卸载相关的Vnode钩子。
// packages/runtime-core/renderer.ts中的代码
function baseCreateRenderer({
  // 省略其他代码...
  
  // 挂载组件
  const mountComponent = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    
    // 获取当前渲染实例
    const instance =
      compatMountInstance ||
      (initialVNode.component = createComponentInstance(
        initialVNode,
        parentComponent,
        parentSuspense
      ))

    // inject renderer internals for keepAlive
    // 为KeepAlive注入私有渲染器
    if (isKeepAlive(initialVNode)) {
      instance.ctx.renderer = internals
    }
     
  }

  // 定义内部渲染器
  const internals = {
    p: patch,
    um: unmount,
    m: move,
    r: remove,
    mt: mountComponent,
    mc: mountChildren,
    pc: patchChildren,
    pbc: patchBlockChildren,
    n: getNextHostNode,
    o: options
  }
}

清空缓存

const KeepAliveImpl = {
  setup(props, { slots }) { 
    // 卸载
    function unmount(vnode{
      // reset the shapeFlag so it can be properly unmounted
      resetShapeFlag(vnode)
      _unmount(vnode, instance, parentSuspense)
    }


    onBeforeUnmount(() => {
      cache.forEach(cached => {
        const { subTree, suspense } = instance
        const vnode = getInnerChild(subTree)
        if (cached.type === vnode.type) {
          // current instance will be unmounted as part of keep-alive's unmount
          resetShapeFlag(vnode)
          // but invoke its deactivated hook here
          const da = vnode.component!.da
          da && queuePostRenderEffect(da, suspense)
          return
        }
        // 清理缓存
        unmount(cached)
      })
    })
  }
}

KeepAlive卸载的时候,会调用onBeforeUnmount生命周期钩子,在此钩子中会遍历cache,执行卸载相关的逻辑。

总结

通过学习分析可以知道,KeepAlive组件是一个抽象组件,抽象组件也是有生命周期的。在KeepAlive组件内部通过onMounted && onUpdated两个生命周期对KeepAlive组件的第一个子节点的Vnode进行缓存,通过watch侦测筛选条件的变化,实现响应式的从cache中增删Vnode。在组件的onBeforeUnmounted阶段,实现缓存的清空。

分类:

后端

标签:

后端

作者介绍

剑大瑞
V1