04-vue3中keep-alive 实现原理解析

码不停息

共 3869字,需浏览 8分钟

 · 2021-12-13

你好我是刘小灰,这是我的第 04 篇原创文章。

前言

最近在做项目中遇到一个很奇葩的问题,就是给路由设置 keep-alive 就是不生效,就在写这篇文章之前,这个 bug 还是没得到解决,所以打算看看 keep-alive 的实现逻辑,看是否从源码中得到些解决这个bug的启发。

怎么去看源码

为了高效,我们不能漫无目的的去看源码,那样会很痛苦且没有任何收获,所以我建议大家在看源码的时候最好是带着问题去看源码,这样会更有目的性,效率自然会大大提高。

从源码中想得到什么

对于 keep-alive 在没有看源码之前我有以下几个疑问

  1. keep-alive 缓存的是什么?vnode 吗?
  2. keep-alive 是把组件缓存到哪里了?
  3. keep-alive 的工作流程是什么?

下面我们就来带着问题来看源码

首先 keep-alive 也是一个组件,我们先找到组件的实例,源码如下所示:

const KeepAliveImpl: ComponentOptions = {
  name`KeepAlive`,
  __isKeepAlivetrue,
  props: {
    include: [StringRegExpArray],
    exclude: [StringRegExpArray],
    max: [StringNumber]
  },
  setup(props: KeepAliveProps, { slots }: SetupContext) {
  ...
  }
}

首先我们可以看到 keep-alive 接受三个props分别是

  1. include 待缓存组件名称
  2. exclude  排除缓存的组件名称
  3. max   允许最大缓存组件数

接下来我们具体看下 setup 函数,如下所示:

setup(props: KeepAliveProps, { slots }: SetupContext) {
    // 获取 keep-alive 实例 并保证一定存在
    const instance = getCurrentInstance()!
    const sharedContext = instance.ctx as KeepAliveContext
    if (!sharedContext.renderer) {
      return slots.default
    }
    // map 用于缓存 组件的vnode
    const cache: Cache = new Map()
    // 用于缓存组件的key
    const keys: Keys = new Set()
    let current: VNode | null = null
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      ;(instance as any).__v_cache = cache
    }
    ...
  }

从代码中我们可以看出 keep-alive 是使用 Map 来缓存组件的,后面的代码是关于钩子函数的执行和对于缓存组件的维护。

40e7d4c9c070fa35327b727910185ad7.webp

watch 后面有一段非常重要的代码就是cacheSubtree函数,用来缓存组件

 const cacheSubtree = () => {
      if (pendingCacheKey != null) {
        cache.set(pendingCacheKey, getInnerChild(instance.subTree))
      }
    }

我们再来看看 getInnerChild 返回的是什么,就证明了 keep-alive 缓存的是不是vnode

function getInnerChild(vnode: VNode{
  return vnode.shapeFlag & ShapeFlags.SUSPENSE ? vnode.ssContent! : vnode
}

可以看出 keep-alive 缓存的就是组件的vnode。

接下来我们再来看看 setup 的 return (渲染)函数

return () => {
      // key
      pendingCacheKey = null
      if (!slots.default) {
        return null
      }
      const children = slots.default()
      const rawVNode = children[0]
      if (children.length > 1) {
        if (__DEV__) {
          warn(`KeepAlive should contain exactly one component child.`)
        }
        current = null
        return children
      } else if (
        !isVNode(rawVNode) ||
        (!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
          !(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
      ) {
        current = null
        return rawVNode
      }
      let vnode = getInnerChild(rawVNode)
      console.log('vnode', vnode === rawVNode)
      //使用vnode的type当做key
      const comp = vnode.type as ConcreteComponent
      const name = getComponentName(
        isAsyncWrapper(vnode)
          ? (vnode.type as ComponentOptions).__asyncResolved || {}
          : comp
      )
      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)
      if (vnode.el) {
        vnode = cloneVNode(vnode)
        if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
          rawVNode.ssContent = vnode
        }
      }
      pendingCacheKey = key
      if (cachedVNode) {
        vnode.el = cachedVNode.el
        vnode.component = cachedVNode.component
        if (vnode.transition) {
          setTransitionHooks(vnode, vnode.transition!)
        }
        vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
        keys.delete(key)
        keys.add(key)
      } else {
        keys.add(key)
        if (max && keys.size > parseInt(max as string, 10)) {
          pruneCacheEntry(keys.values().next().value)
        }
      }
      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
      current = vnode
      return rawVNode
    }
  }

主要是对插槽的一些判断以及对props函数的处理,最后返回也是需要渲染的vnode

总结

从源码中可以看出 keep-alive 就是通过 Map 缓存组件的 vnode 以及会给每个组件做一个标识,这样在渲染的时候就不会再执行组件的初始化函数。


浏览 111
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报