keep-alive的设计初衷

有些业务场景需要根据不同的判断条件,动态地在多个组件之间切换。频繁的组件切换会导致组件反复渲染,如果组件包含有大量的逻辑和dom节点,极易造成性能问题。其次,切换后组件的状态也会完全丢失。keep-alive的设计初衷就是为了保持组件的状态,避免组件的重复渲染。

为什么keep-alive可以直接使用

开发者无需注册和引入,直接可以在模板中使用。 跟开发者使用Vue.component自定义的组件不同,keep-alive无需注册,在模板中直接可以使用,如下所示:

<keep-alive>
 <component :is="view"></component>
</keep-alive>

这是因为keep-alive是vue的内置组件,已经在vue中提前定义。

// 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 () {
  // keep-alive的销毁,将所有缓存的组件清除
  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 () {
  // keepAlive组件本身不会被渲染成dom节点,其render方法的处理逻辑的是将其包裹的组件的vnode返回
  const slot = this.$slots.default
  // 获取第一个组件子节点
  const vnode: VNode = getFirstComponentChild(slot)
  const componentOptions: "htmlcode">
// 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 () {
  // keep-alive的销毁,将所有缓存的组件清除
  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 () {
  // keepAlive组件本身不会被渲染成dom节点,其render方法的处理逻辑的是将其包裹的组件的vnode返回
  const slot = this.$slots.default
  // 获取第一个组件子节点
  const vnode: VNode = getFirstComponentChild(slot)
  const componentOptions: "htmlcode">
// core/components/index.js
import KeepAlive from './keep-alive'

export default {
 KeepAlive
}

// core/global-api/index.js

// 导入内置组件
import builtInComponents from '../components/index'

/**
 * 为Vue添加全局方法和属性
 * @param {GlobalAPI} Vue 
 */
export function initGlobalAPI (Vue: GlobalAPI) {
 
 // ...省略了无关代码
 
 Vue.options = Object.create(null)
 // 添加内置组件keep-alive
 extend(Vue.options.components, builtInComponents)
}

buildInComponents中包含了keep-alive的定义。在initGlobalAPI方法中,将内置组件添加到了 vue的全局变量中。

extend(A, B)是个简单的对象属性复制方法。将对象B中的属性复制到对象A中。

keep-alive是如何保持组件状态的

为了保持组件状态,keep-alive设计了缓存机制。

我们知道,模板中的每个HTML标签在vue中由相应的vnode节点对象来表示。如果是HTML标签是组件标签,需要为该标签的vnode创建一个组件实例。组件实例负责组件内的HTML模板的编译和渲染。因此相比于普通HTML标签的vnode节点,组件vnode节点会存在componentOptions和 componentInstance 这两个属性中保存组件选项对象和组件实例的引用。

首先,我们从keep-alive组件的实现代码中可以看到在组件的created钩子中设计了缓存机制:

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

keep-alive设置了cache和keys两个属性来缓存子组件。其中cache中的每项是一个以所包裹的组件的组件名为key,包裹组件对应的vnoded为值的对象。keys的每一项是其所包裹的组件的组件名。

render 函数是vue实例和vue组件实例中用来创建vnode的方法。我们在实际的应用中,一般是通过template和el来指定模板,然后由vue将模板编译成render函数。如果用户希望能更灵活地控制vnode的创建可以提供自定义的render函数。

render () {
  const slot = this.$slots.default
  // 获取第一个组件子节点
  const vnode: VNode = getFirstComponentChild(slot)
  const componentOptions: "htmlcode">
 <div id="app">
  <keep-alive><component :is="view"></component></keep-alive>
  <button @click="view = view =='count'">切换组件</button>
</div>
Vue.component("count", {
  data() {
    return {
      count:0
    };
  },
  template: "<div @click='count+=1'>点了我 {{count}} 次</div>"
});
  
Vue.component("any", {
  template: "<div>any</div>"
});

new Vue({
  el: "#app",
  data: {
   view: "count"
  }
});

由于view默认值是count,因此keep-alive包裹的子组件是count。此时keep-alive的缓存中为空,因此会把组件count的vnode添加到缓存。缓存结果为

cache = {1::count: