Vue3核心源码解析 (五) : 内置组件<keep-alive>

  <keep-alive>是Vue.js的一个内置组件,可以使被包含的组件保留状态或避免重新渲染。下面来分析源码runtime-core/src/components/KeepAlive.ts的实现原理。
  在setup方法中会创建一个缓存容器和缓存的key列表,其代码如下:

  setup(props: KeepAliveProps, { slots }: SetupContext) {
// keep-alive组件的上下文对象
    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 as KeepAliveContext

    // 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 (__SSR__ && !sharedContext.renderer) {
      return () => {
        const children = slots.default && slots.default()
        return children && children.length === 1 ? children[0] : children
      }
    }
/* 缓存对象 */
    const cache: Cache = new Map()
    const keys: Keys = new Set()
       // 替换内容
       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方法:

    return () => {
      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)
      const comp = vnode.type as ConcreteComponent

      // for async components, name check should be based in its loaded
      // inner component if available
 // 获取组件名称,优先获取组件的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)

      // clone vnode if it's reused because we are going to mutate it
      if (vnode.el) {
        vnode = cloneVNode(vnode)
        if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
          rawVNode.ssContent = vnode
        }
      }
      // #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 = key
 // 如果已经缓存了,则直接从缓存中获取组件实例给vnode,若还未缓存,则先进行缓存
      if (cachedVNode) {
        // copy over mounted state
        vnode.el = cachedVNode.el
        vnode.component = cachedVNode.component
 // 执行transition
        if (vnode.transition) {
          // recursively update transition hooks on subTree
          setTransitionHooks(vnode, vnode.transition!)
        }
//  设置shapeFlag标志位,为了避免执行组件mounted方法
        // avoid vnode being mounted as fresh
        vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
        // make this key the freshest
   // 重新设置一下key保证最新
        keys.delete(key)
        keys.add(key)
      } else {
        keys.add(key)
        // prune oldest entry
  // 当超出max值时,清除缓存
        if (max && keys.size > parseInt(max as string, 10)) {
          pruneCacheEntry(keys.values().next().value)
        }
      }
      // avoid vnode being unmounted
      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

      current = vnode
      return isSuspense(rawVNode.type) ? rawVNode : vnode
    }

  在上面的代码中,当缓存的个数超过max(默认值为10)的值时,就会清除旧的数据,这其中就包含<keep-alive>的缓存更新策略,其遵循了LRU(Least Rencently Used)算法。

1. LRU算法

  LRU算法根据数据的历史访问记录来淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的概率也更高”。利用这个思路,我们可以对<keep-alive>中缓存的组件数据进行删除和更新,其算法的核心实现如下:


LRU

上面的代码中,主要利用map来存储缓存数据,利用map.keyIterator.next()来找到最久没有使用的key对应的数据,从而对缓存进行删除和更新。

2. 缓存VNode对象

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

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

  这里的程序逻辑是动态监听include和exclude的改变,从而动态地维护之前创建的缓存对象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 || !isSameVNodeType(cached, current)) {
        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.activate和sharedContext.deactivate来进行内容的替换,其核心是move方法,代码如下:

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容