(看完必会系列)Vue源码解析之 keep-alive

keep-alive是Vue.js的一个内置组件。它能够不活动的组件实例保存在内存中,我们来探究一下它的源码实现。

首先回顾下使用方法

举个栗子

<keep-alive>
    <component-a v-if="isShow"></component-a>
    <component-b v-else></component-b>
</keep-alive>
<button @click="test=handleClick">请点击</button>
复制代码
export default {
    data () {
        return {
            isShow: true
        }
    },
    methods: {
        handleClick () {
            this.isShow = !this.isShow;
        }
    }
}
复制代码

在点击按钮时,两个组件会发生切换,但是这时候这两个组件的状态会被缓存起来,比如:组件中都有一个input标签,那么input标签中的内容不会因为组件的切换而消失。

属性支持

keep-alive组件提供了includeexclude两个属性来允许组件有条件地进行缓存,二者都可以用逗号分隔字符串、正则表达式或一个数组来表示。

举个例子:

  • 缓存name为a的组件。
<keep-alive include="a">
  <component></component>
</keep-alive>
复制代码
  • 排除缓存name为a的组件。
<keep-alive exclude="a">
  <component></component>
</keep-alive>
复制代码

当然 props 还定义了 max,该配置允许我们指定缓存大小。

keep-alive 源码实现

说完了keep-alive组件的使用,我们从源码角度看一下keep-alive组件究竟是如何实现组件的缓存的呢?

创建和销毁阶段

首先看看 keep-alive 的创建和销毁阶段做了什么事情:

created () {
    /* 缓存对象 */
    this.cache = Object.create(null)
},
destroyed () {
    for (const key in this.cache) {
        pruneCacheEntry(this.cache[key])
    }
},
复制代码
  • keep-alive 的创建阶段: created钩子会创建一个cache对象,用来保存vnode节点。
  • 在销毁阶段:destroyed 钩子则会调用pruneCacheEntry方法清除cache缓存中的所有组件实例。

pruneCacheEntry 方法的源码实现

/* 销毁vnode对应的组件实例(Vue实例) */
function pruneCacheEntry (vnode: ?VNode) {
  if (vnode) {
    vnode.componentInstance.$destroy()
  }
}
复制代码

因为keep-alive会将组件保存在内存中,并不会销毁以及重新创建,所以不会重新调用组件的created等方法,因此keep-alive提供了两个生命钩子,分别是activateddeactivated。用这两个生命钩子得知当前组件是否处于活动状态。(稍后会看源码如何实现)

渲染阶段
render () {
    /* 得到slot插槽中的第一个组件 */
    const vnode: VNode = getFirstComponentChild(this.$slots.default)

    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
        // 获取组件名称,优先获取组件的name字段,否则是组件的tag
        const name: ?string = getComponentName(componentOptions)

        // 不需要缓存,则返回 vnode
        if (name && (
        (this.include && !matches(this.include, name)) ||
        (this.exclude && matches(this.exclude, name))
        )) {
            return vnode
        }
        const key: ?string = vnode.key == null
            ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
            : vnode.key
        if (this.cache[key]) {
           // 有缓存则取缓存的组件实例
            vnode.componentInstance = this.cache[key].componentInstance
        } else {
            // 无缓存则创建缓存
            this.cache[key] = vnode
            
            // 创建缓存时
            // 如果配置了 max 并且缓存的长度超过了 this.max
            // 则从缓存中删除第一个
            if (this.max && keys.length > parseInt(this.max)) {
              pruneCacheEntry(this.cache, keys[0], keys, this._vnode)
            }
        }
        // keepAlive标记
        vnode.data.keepAlive = true
    }
    return vnode
}
复制代码

render 做了以下事情:

  1. 通过getFirstComponentChild获取第一个子组件,获取该组件的name(存在组件名则直接使用组件名,否则会使用tag)
  2. 将name通过include与exclude属性进行匹配,匹配不成功(说明不需要缓存)则直接返回vnode
  3. 匹配成功则尝试获取缓存的组件实例
  4. 若没有缓存该组件,则缓存该组件
  5. 缓存超过最大值会删掉第一个缓存

name 匹配的方法(校验是逗号分隔的字符串还是正则)

/* 检测name是否匹配 */
function matches (pattern: string | RegExp, name: string): boolean {
  if (typeof pattern === 'string') {
    /* 字符串情况,如a,b,c */
    return pattern.split(',').indexOf(name) > -1
  } else if (isRegExp(pattern)) {
    /* 正则 */
    return pattern.test(name)
  }
  /* istanbul ignore next */
  return false
}
复制代码

如果在中途有对 includeexclude 进行修改该怎么办呢?

作者通过 watch 来监听 includeexclude,在其改变时调用 pruneCache 以修改 cache 缓存中的缓存数据。

watch: {
    /* 监视include以及exclude,在被修改的时候对cache进行修正 */
    include (val: string | RegExp) {
        pruneCache(this.cache, this._vnode, name => matches(val, name))
    },
    exclude (val: string | RegExp) {
        pruneCache(this.cache, this._vnode, name => !matches(val, name))
    }
},
复制代码

那么 pruneCache 做了什么?

// 修补 cache
function pruneCache (cache: VNodeCache, current: VNode, filter: Function) {
  for (const key in cache) {
    // 尝试获取 cache中的vnode
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
      const name: ?string = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) { // 重新筛选组件
        if (cachedNode !== current) { // 不在当前 _vnode 中
          pruneCacheEntry(cachedNode) // 调用组件实例的 销毁方法
        }
        cache[key] = null // 移除该缓存
      }
    }
  }
} 
复制代码

pruneCache方法 遍历cache中的所有项,如果不符合规则则会销毁该节点并移除该缓存

进阶

再回顾下源码,在 src/core/components/keep-alive.js

export default {
  name: 'keep-alive,
  abstract: true,

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },

  created () {
    this.cache = Object.create(null)
    this.keys = []
  },

  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        (include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      const key: ?string = vnode.key == null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        remove(keys, key)
        keys.push(key)
      } else {
        cache[key] = vnode
        keys.push(key)
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}
复制代码

现在不加注释也应该大部分都能看懂了?

顺便提下 abstract 这个属性,若 abstracttrue,则表示组件是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。

那么为什么在组件有缓存的时候不会再次执行组件的 createdmounted 等钩子函数呢?

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    // 进入这段逻辑
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      const mountedNode: any = vnode 
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
  // ...
}
复制代码

看上面了代码, 满足 vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive 的逻辑就不会执行$mount的操作,而是执行prepatch

那么 prepatch 究竟做了什么?

  // 不重要内容都省略...
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    // 会执行这个方法
    updateChildComponent(//...)
  },
  // ...
复制代码

其中主要是执行了 updateChildComponent 函数。

function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  const hasChildren = !!(
    renderChildren ||          
    vm.$options._renderChildren ||
    parentVnode.data.scopedSlots || 
    vm.$scopedSlots !== emptyObject 
  )

  // ...
  if (hasChildren) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }
}
复制代码

keep-alive 组件本质上是通过 slot 实现的,所以它执行 prepatch 的时候,hasChildren = true,会触发组件的 $forceUpdate 逻辑,也就是重新执行 keep-alive 的 render 方法

然鹅,根据上面讲的 render 方法源码,就会去找缓存咯。

那么,<keep-alive> 的实现原理就介绍完了

最后

  1. 原创不易点个赞呗
  2. 欢迎关注公众号「前端进阶课」认真学前端,一起进阶。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值