手写Vue2源码(十二)—— keep-alive

425 阅读6分钟

前言

通过手写Vue2源码,更深入了解Vue;

在项目开发过程,一步一步实现Vue核心功能,我会将不同功能放到不同分支,方便查阅;

另外我会编写一些开发文档,阐述编码细节及实现思路;

源码地址:手写Vue2源码

使用场景

频繁的组件切换,或者路由切换,需要对组件进行缓存,避免组件的重复创建。

<keep-alive :include='whiteList' :exclude='blackList' :max='count'>
    <component :is='component'></component>
</keep-alive>
<keep-alive :include='whiteList' :exclude='blackList' :max='count'>
    <router-view></router-view>
</keep-alive>

keep-alive注册流程

在定义Vue时,调用initGlobalAPI(Vue)

// src/core/index.js
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
initGlobalAPI(Vue)
export default Vue

initGlobalAPI()中定义了很多全局的方法和数据(例如:Vue.optionsVue.configVue.utilVue.setVue.deleteVue.nextTickVue.observableVue.useVue.mixinVue.extend等),并注册了全局组件 keep-alive

// src/core/global-api/index.js
import builtInComponents from '../components/index'
import {
  extend
} from '../util/index'

export function initGlobalAPI (Vue: GlobalAPI) {
  // ...略

  // extend定义在 src/shared/util.js中
  // 作用是将builtInComponents合并到Vue.options.components
  extend(Vue.options.components, builtInComponents)
}

在builtInComponents中引入需要定义在全局的组件:

// src/core/components/index.js
import KeepAlive from './keep-alive'
export default {
  KeepAlive
}

小结:

  1. keep-alive是一个全局组件
  2. 在Vue构造函数的initGlobalAPI(Vue)方法中,将keep-alive组件置入Vue.options.components中;
  3. 组件实例化时会将Vue.options与组件实例的vm.options合并;components的合并采用的是原型继承,所以可以在任意组件中使用keep-alive

keep-alive的实现原理

// src/core/components/keep-alive.js
export default {
  name: 'keep-alive',
  abstract: true,   // 不会放到对应的lifecycle,也不会创建dom

  props: {
    include,  // 白名单
    exclude,  // 黑名单
    max: [String, Number]   // 缓存的最大个数
  },

  methods: {
    // 往cache中缓存vnode,往keys中缓存vnode的key
    cacheVNode() {
      const { cache, keys, vnodeToCache, keyToCache } = this
      if (vnodeToCache) {
        const { tag, componentInstance, componentOptions } = vnodeToCache
        cache[keyToCache] = {
          name: getComponentName(componentOptions),
          tag,
          componentInstance,
        }
        keys.push(keyToCache)
        // 超出长度则删除第一项
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
        this.vnodeToCache = null
      }
    }
  },

  created () {
    this.cache = Object.create(null)    // 缓存组件vnode的对象
    this.keys = []  // 缓存的key列表
  },

  // 删除缓存的cache列表和keys列表
  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  // 监控include/exclude,动态更新缓存列表
  mounted () {
    this.cacheVNode()   // 缓存组件
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  updated () {
    this.cacheVNode()
  },

  render () {
    const slot = this.$slots.default    // 获取包裹的默认插槽
    const vnode: VNode = getFirstComponentChild(slot)   // 获取第一个组件
    const componentOptions = vnode && vnode.componentOptions    // 获取组件的componentOptions:{Ctor,children,tag,name...}
    if (componentOptions) {
      const name = getComponentName(componentOptions)
      const { include, exclude } = this
      // 如果不走缓存
      if (
        // not included 不包含
        (include && (!name || !matches(include, name))) ||
        // excluded 排除
        (exclude && name && matches(exclude, name))
      ) {
        return vnode    // 不需要缓存则直接返回虚拟节点即可
      }

      const { cache, keys } = this
      const key = vnode.key == null
        // same constructor may get registered as different local components
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
        
      // 如果在cache中缓存过该组件
      if (cache[key]) {
        // 通过key,找到缓存,获取实例
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)   // 把keys数组里面的key删掉
        keys.push(key)  // 把它放在数组末尾
      } else {
        cache[key] = vnode; // 没找到就换存下来
        keys.push(key); // 把它放在数组末尾
        // prune oldest entry  // 如果超过最大值就把数组第0项删掉
        if (this.max && keys.length > parseInt(this.max)) {
          // LRU算法:删除缓存第0项的组件和key
          pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }

      vnode.data.keepAlive = true   // 标记虚拟节点已经被缓存
    }
    // 返回虚拟节点
    return vnode || (slot && slot[0])
  }
}

小结:

  1. keep-alive缓存的是组件的vnode
  2. 通过 LRU 算法对组件进行缓存

缓存中的LRU算法

LRU

  1. LRU全称:Least Recently Used
  2. 将新数据从尾部插入到this.keys
  3. 每当缓存命中时(即缓存数据被访问时),则将其移到this.keys尾部
  4. this.keys满的时候,将头部数据丢弃

keep-alive中的LRU算法:

  1. 当没有缓存时,将vnode的key推进this.keys中的末尾,并在cache中设置vnode.key属性及其对应的vnode
  2. 设置缓存时,如果长度大于max,则删除this.keys中的第0项,并删除cache中key对应的vnode
  3. 当有缓存时,获取缓存的数据,并将该缓存组件的key放到this.keys中的末尾

keep-alive组件的渲染

首次渲染

  1. keep-alive组件执行render()函数,可以看到首次执行render()函数的时候,没有缓存任何子组件,keep-alive缓存的子组件的vnode.componentInstance为null,且vnode.data.keepAlive为true。
  2. keep-alive组件的patch()过程,对于内部缓存的组件会调用createComponent方法,实际上执行的是缓存组件的init()函数
    function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
        let i = vnode.data
        if (isDef(i)) {
            // 初次渲染时vnode.componentInstance为null,则isReactivated为false
            const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
            if (isDef(i = i.hook) && isDef(i = i.init)) {
                i(vnode, false /* hydrating */)
            }
            // after calling the init hook, if the vnode is a child component
            // it should've created a child instance and mounted it. the child
            // component also has set the placeholder vnode's elm.
            // in that case we can just return the element and be done.
            if (isDef(vnode.componentInstance)) {
                initComponent(vnode, insertedVnodeQueue)
                insert(parentElm, vnode.elm, refElm)
                if (isTrue(isReactivated)) {
                    reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
                }
                return true
            }
        }
    }
    
  3. 我们来看下init()函数,在init()函数里面会对vnode.componentInstancevnode.data.keepAlive的值做判断。由第一步的值可知,首次渲染的时候会走else里面的逻辑,执行createComponentInstanceForVnode()创建新的子组件,接下来会去执行子组件的$mount过程(会调用正常组件的所有生命周期),去创建子组件的虚拟Vnode。
    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 {
             const child = vnode.componentInstance = createComponentInstanceForVnode(
                 vnode,
                 activeInstance
             )
             child.$mount(hydrating ? vnode.elm : undefined, hydrating)
         }
     },
     // prepatch
     // insert
     // destroy
    }
    

非首次渲染

会触发keep-alive的缓存机制

  1. 触发keep-alive的render()函数,此时会命中缓存,所以keep-alive缓存的子组件的vnode.componentInstance为组件A,且vnode.data.keepAlive为true。
  2. 执行keep-alive组件的patch()过程,实际上执行的是init()函数,这时vnode.componentInstancevnode.data.keepAlive都为true,所以不会执行createComponentInstanceForVnode()创建新的子组件,而是直接去缓存中取子组件。并执行componentVNodeHooks.prepatch(mountedNode, mountedNode)
  3. 当我们的代码重新执行到createComponent时,此时isReactivated为true,会执行reactivateComponent()方法
    function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
     let i = vnode.data
     if (isDef(i)) {
       const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
       if (isDef(i = i.hook) && isDef(i = i.init)) {
         i(vnode, false /* hydrating */)
       }
       if (isDef(vnode.componentInstance)) {
         initComponent(vnode, insertedVnodeQueue)
         insert(parentElm, vnode.elm, refElm)
         if (isTrue(isReactivated)) {
           // 非首次渲染会执行到这一步
           reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
         }
         return true
       }
     }
    }
    
  4. 执行reactivateComponent()函数,最后通过执行insert(parentElm, vnode.elm, refElm)就把缓存的DOM对象直接插入到目标元素中,这样就完成了在数据更新的情况下的渲染过程。
    function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
     let i
     // hack for #4339: a reactivated component with inner transition
     // does not trigger because the inner node's created hooks are not called
     // again. It's not ideal to involve module-specific logic in here but
     // there doesn't seem to be a better way to do it.
     let innerNode = vnode
     while (innerNode.componentInstance) {
       innerNode = innerNode.componentInstance._vnode
       if (isDef(i = innerNode.data) && isDef(i = i.transition)) {
         for (i = 0; i < cbs.activate.length; ++i) {
           cbs.activate[i](emptyNode, innerNode)
         }
         insertedVnodeQueue.push(innerNode)
         break
       }
     }
     // unlike a newly created component,
     // a reactivated keep-alive component doesn't insert itself
     insert(parentElm, vnode.elm, refElm)
    }
    

抽象组件

定义

不会在DOM树中渲染(真实或者虚拟都不会),不会渲染为一个DOM元素,也不会出现在父组件链中——你永远在 this.$parent 中找不到。它有一个属性 abstract 为 true,表明是它一个抽象组件。

特点:抽象组件的子组件,会选取抽象组件的上一级非抽象组件作为父级

常见的抽象组件:<keep-alive><transition><transition-group>等.

抽象组件是如何忽略掉父子关系

// src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
    const options= vm.$options
    // 找到第一个非abstract父组件实例
    let parent = options.parent
    if (parent && !options.abstract) {
        while (parent.$options.abstract && parent.$parent) {
              parent = parent.$parent
        }
        parent.$children.push(vm)
    }
    vm.$parent = parent
    // ...
}

Vue在初始化生命周期的时候,为组件实例建立父子关系会根据abstract属性决定是否忽略某个组件。在抽象组件中,设置了abstract:true,那Vue就会跳过该组件实例。 最后构建的组件树中就不会包含keep-alive组件,那么由组件树渲染成的DOM树自然也不会有keep-alive相关的节点了。

缓存组件的生命周期钩子

只执行一次的钩子——created、mounted、destroyed等

被缓存的组件实例会为其设置keepAlive= true,而在初始化组件钩子函数中:

// src/core/vdom/create-component.js
const componentVNodeHooks = {
    init (vnode: VNodeWithData, hydrating: boolean): ?boolean{
        if (
         vnode.componentInstance &&       
         !vnode.componentInstance._isDestroyed &&
         vnode.data.keepAlive
        ) {
          // keep-alive components, treat as a patch
          const mountedNode:any = vnode
          componentVNodeHooks.prepatch(mountedNode, mountedNode)
        } else {
          const child = vnode.componentInstance = createComponentInstanceForVnode (vnode, activeInstance)
        }
    }
}

初次渲染时,vnode.componentInstance为null,会创建缓存组件的vnode,执行完整的$mount流程,期间会调用非缓存组件的所有生命周期方法,如created、mounted、updated等。

当非首次渲染,vnode.componentInstance为缓存的vnode、keepAlive为true,此时不再进入$mount过程,那mounted之前的所有钩子函数(beforeCreate、created、mounted)都不再执行。只会执行componentVNodeHooks.prepatch(mountedNode, mountedNode),直接走patch流程。

可重复执行的钩子——activated、deactivated

在patch的阶段,最后会执行invokeInsertHook函数,而这个函数就是去调用组件实例(VNode)自身的insert钩子:

// src/core/vdom/patch.js
function invokeInsertHook (vnode, queue, initial) {
      if (isTrue(initial) && isDef(vnode.parent)) {
          vnode.parent.data,pendingInsert = queue
      } else {
         for(let i =0; i<queue.length; ++i) {
                queue[i].data.hook.insert(queue[i]) // 调用VNode自身的insert钩子函数
         }
      }
}

再看insert钩子:

// src/core/vdom/create-component.js
const componentVNodeHooks = {
      // init()
     insert (vnode: MountedComponentVNode) {
           const { context, componentInstance } = vnode
           if (!componentInstance._isMounted) {
                 componentInstance._isMounted = true
                 callHook(componentInstance, 'mounted')
           }
           if (vnode.data.keepAlive) {
                 if (context._isMounted) {
                     queueActivatedComponent(componentInstance)
                 } else {
                      activateChildComponent(componentInstance, true/* direct */)
                 }
          }
         // ...
     }
     // destroy()
}

在这个钩子里面,调用了activateChildComponent函数递归地去执行所有子组件的 activated 钩子函数:

// src/core/instance/lifecycle.js
export function activateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = false
    if (isInInactiveTree(vm)) {
      return
    }
  } else if (vm._directInactive) {
    return
  }
  if (vm._inactive || vm._inactive === null) {
    vm._inactive = false
    for (let i = 0; i < vm.$children.length; i++) {
      activateChildComponent(vm.$children[i])
    }
    callHook(vm, 'activated')
  }
}

相反地,deactivated 钩子函数也是一样的原理,在组件实例(VNode)的destroy钩子函数中调用deactivateChildComponent函数。

系列文章