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

推荐阅读更多精彩内容

  • keep-alive是Vue.js的一个内置组件。它能够把不活动的组件实例保存在内存中,而不是直接将其销毁,它是一...
    指尖跳动阅读 10,057评论 0 6
  • 一、前言 本文介绍的内容包括: keep-alive用法:动态组件&vue-router keep-alive源码...
    amCow阅读 165,145评论 5 132
  • 非本人所写,只为方便阅读参考地址:https://juejin.im/book/5a36661851882538e...
    无花无酒_3cd3阅读 2,581评论 0 1
  • keep-alive 这个功能是vue特有的,在react中,我暂时没有找过相关组件,所以第一次用这个标签时,觉得...
    zdxhxh阅读 653评论 2 0
  • 斜阳疏影怜清秋, 雁游烟霞尘叶羞, 空山仙岭琢云袅, 飘洒水墨绘神州。
    嘎嘎乐翻天阅读 215评论 1 3