likes
comments
collection
share

[Vue 源码] keep-alive 组件逻辑分析(上)

作者站长头像
站长
· 阅读数 38

keep-alive

基本用法

keep 的使用只需要在动态组件的最外层添加标签即可

<div id="app">
    <button @click="changeTabs('child1')">child1</button>
    <button @click="changeTabs('child2')">child2</button>
    <keep-alive>
        <component :is="chooseTabs">
        </component>
    </keep-alive>
</div>
var child1 = {
    template: '<div><button @click="add">add</button><p>{{num}}</p></div>',
    data() {
        return {
            num: 1
        }
    },
    methods: {
        add() {
            this.num++
        }
    },
}
var child2 = {
    template: '<div>child2</div>'
}
var vm = new Vue({
    el: '#app',
    components: {
        child1,
        child2,
    },
    data() {
        return {
            chooseTabs: 'child1',
        }
    },
    methods: {
        changeTabs(tab) {
            this.chooseTabs = tab;
        }
    }
})

当动态组件在 child1 和 child2 之间来回切换时,第二次以后在切到 child1 组件, child1 保留着原来的数据状态。

从模版编译到虚拟 DOM

内置组件和普通组件在编译过程中没有区别,不管是哪只组件还是用户定义组件,本质上组件在模版编译成 render 函数的处理方式是一致的。这里不在分析 render 函数的生成过程。

在拿到 render 函数之后,开始生成虚拟 DOM , 由于 keep-alive 是组件,所以会调用 createComponent 函数去创建子组件的虚拟 DOM , 在 createComponent 环节中,和创建普通组件不同之处在于, keep-alive 的虚拟 DOM 会去除多余的属性,除了 slot 属性之外(slot 属性也在 2.6 版本之后被废弃),其他属性都没有意义。在 keep-alive 组件的虚拟 DOM 上,存在一个 abstract 属性作为抽象组件的标志。

// 创建子组件Vnode过程
function createComponent(Ctordata,context,children,tag) {
  // abstract是内置组件(抽象组件)的标志
  if (isTrue(Ctor.options.abstract)) {
    // 只保留slot属性,其他标签属性都被移除,在vnode对象上不再存在
    var slot = data.slot;
    data = {};
    if (slot) {
        data.slot = slot;
    }
  }
}

初次渲染

keep-alive 之所以特别,是因为他不会重复渲染相同的组件,只会利用初次渲染保留的缓存去更新节点,为了更全面的了解 keep-alive 的实现原理,我们冲他的首次渲染开始分析。

流程分析

和普通组件相同的是, Vue 会拿到前面生成的虚拟 DOM 对象,执行真实节点的创建过程,也就是 patch 过程,在创建节点过程中, keep-alive 的虚拟 DOM 会被认为是一个组件的 vnode ,因此会进入 createComponent 函数,在该函数中对 keep-alive 组件进行初始化和实例化。


function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    // isReactivated 用来判断组件是否缓存
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      // 执行组件初始化的内部钩子 init
      i(vnode, false /* hydrating */)
    }
    if (isDef(vnode.componentInstance)) {
      // 其中一个作用就是保留真实 DOM 的虚拟 DOM 中
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

keep-alive 组件会先调用内部的 init 钩子进行初始化操作,

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      // 将组件实例复制给虚拟 DOM 的 componentInstance 属性
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      // 创建组件实例之后进行挂载
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    // ...
  },

  insert (vnode: MountedComponentVNode) {
    // ...
  },

  destroy (vnode: MountedComponentVNode) {
    //...
  }
}

在第一次执行,组件的 vnode 对象中没有 componentInstance 属性, vnode.data.keepAlive 也没有值,所以会调用 createComponentInstanceForVnode 方法进行组件实例化并将组件实例复制给 vnode 的 componentInstance 属性,最终执行组件实例的 $mount 方法进行实例挂载。

createComponentInstanceForVnode 就是组件实例化的过程,这一过程前面已经分析过了,主要包含选项合并 、 初始化事件 、 生命周期等初始化操作。

内置组件选项

在使用组件的时候经常利用对象的形式定义组件选项,包括 datamethodcomputed 等,并在父组件或者全局中进行注册,来看一下 keep-alive 的具体选项

export default {
  name: 'keep-alive',
  abstract: true,
  // keep-alive 组件允许的 props 值
  props: {
    include: patternTypes,  // 需要进行缓存的组件名称
    exclude: patternTypes,  // 不需要进行缓存的组件名称
    max: [String, Number]   // 最大缓存数量,超过该数量时, 依据 lru 算法进行替换
  },

  created () {
    // 缓存组件 vnode
    this.cache = Object.create(null)
    // 缓存组件名称
    this.keys = []
  },

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

  mounted () {
    // 对于动态的 include 和 exclude , 需要进行监听
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  // 渲染函数
  render () {
    // 拿到 keep-alive 下插槽的值
    const slot = this.$slots.default
    // 获取第一个 vnode 节点
    const vnode: VNode = getFirstComponentChild(slot)
    // 拿到第一个组件实例
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      // 拿到第一个子组件 vnode 的 name 属性
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        // 判断子组件是否需要进行缓存,不需要缓存时直接返回 vnode 对象
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      const key: ?string = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      if (cache[key]) {
        // 命中缓存,
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        // 删除 key 之后在重新添加,最近使用到的缓存会放在数组后面,这是 lru 算法思想,后面详细分析
        remove(keys, key)
        keys.push(key)
      } else {
        // 初次渲染,缓存 vnode
        cache[key] = vnode
        keys.push(key)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      // 为缓存组件加上标志
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}

keep-alive 组件和普通组件在选项上是类似的, keep-alive 组件使用了 render 函数而不是 template 模版。 keep-alive 组件本质上只是存缓存和取缓存的过程,并没有实际的节点渲染。

缓存 VNode

keep-alive 组件在实例化之后会进行组件的挂载,而挂载过程又回到了 vm._rendervm_update 的过程。 由于 keep-alive 拥有 render 函数,所以我们直接分析 render 函数的实现

获取 keep-alive 组件插槽的内容

首先是通过 getFirstComponentChild 方法获取 keep-alive 下插槽的内容,也就是 keep-alive 组件需要渲染的子组件,

export function getFirstComponentChild (children: ?Array<VNode>): ?VNode {
  if (Array.isArray(children)) {
    for (let i = 0; i < children.length; i++) {
      const c = children[i]
      // 组件实例存在,则返回,理论上返回第一个组件的 vnode
      if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
        return c
      }
    }
  }
}

缓存组件

拿到子组件实例后,需要判断是否满足缓存的匹配条件,匹配条件可以使用数组、字符串、正则

const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
  // 判断子组件是否需要进行缓存,不需要缓存时直接返回 vnode 对象
  // not included
  (include && (!name || !matches(include, name))) ||
  // excluded
  (exclude && name && matches(exclude, name))
) {
  return vnode
}

function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
  // 允许使用数组的形式
  if (Array.isArray(pattern)) {
    return pattern.indexOf(name) > -1
  } else if (typeof pattern === 'string') {
    // 允许使用字符串的形式
    return pattern.split(',').indexOf(name) > -1
  } else if (isRegExp(pattern)) {
    // 允许使用正则的形式
    return pattern.test(name)
  }
  /* istanbul ignore next */
  return false
}

如果组件不满足缓存的要求,则会之间返回组件的 vnode ,然后进入挂载流程

render 函数关键的一步就是缓存 vnode ,由于第一次执行 render 函数,选项中的 cachekeys 都没有数据,无法命中缓存,因此,在第一次渲染 keep-alive 组件是,会将需要渲染的子组件 vnode 进行缓存。将已经缓存的 vnode 搭上标志,并将子组件的 vnode 进行返回, vnode.data.keepAlive = true

真实节点的保存

再回到 crateComponent 的逻辑,在 createComponent 方法中,首先会执行 keep-alive 组件的初始化流程,也包括了子组件的挂载,之后在 createComponent 方法中拿到了 keep-alive 组件的实例,接下来重要的一步就是将真实 DOM 保存在 vnode 中。

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    // isReactivated 用来判断组件是否存在缓存
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      // 执行组件初始化的内部钩子 init
      i(vnode, false /* hydrating */)
    }
    if (isDef(vnode.componentInstance)) {
      // 其中一个作用就是保留真实 DOM 的虚拟 DOM 中
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

function initComponent (vnode, insertedVnodeQueue) {
  if (isDef(vnode.data.pendingInsert)) {
    insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
    vnode.data.pendingInsert = null
  }
  // 将真实 DOM 保存到 vnode 中
  vnode.elm = vnode.componentInstance.$el
  if (isPatchable(vnode)) {
    invokeCreateHooks(vnode, insertedVnodeQueue)
    setScope(vnode)
  } else {
    // empty component root.
    // skip all element-related modules except for ref (#3455)
    registerRef(vnode)
    // make sure to invoke the insert hook
    insertedVnodeQueue.push(vnode)
  }
}

在进行组件缓存是,需要将组件的真实 DOM 节点保存到 vnode 对象上,而保存大量的 DOM 元素会非常耗费性能,因此我们需要严格控制缓存组件的数量,另外在缓存策略上也需要做优化。

keep-alive 组件的初次渲染做一个总结:内置的 keep-alive 组件,在子组件第一次进行渲染是将 vnode 和真实 DOM 进行了缓存

转载自:https://juejin.cn/post/7204243582378377276
评论
请登录