彻底揭秘keep-alive原理

栏目: 编程语言 · 发布时间: 5年前

内容简介:原文链接:本文介绍的内容包括:keep-alive是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中;使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

原文链接: github.com/qi...

本文介绍的内容包括:

  • keep-alive用法:动态组件&vue-router
  • keep-alive源码解析
  • keep-alive组件及其包裹组件的钩子
  • keep-alive组件及其包裹组件的渲染

二、keep-alive介绍与应用

2.1 keep-alive是什么

keep-alive是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中;使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

2.2 一个场景

用户在某个列表页面选择筛选条件过滤出一份数据列表,由列表页面进入数据详情页面,再返回该列表页面,我们希望:列表页面可以保留用户的筛选(或选中)状态。keep-alive就是用来解决这种场景。当然keep-alive不仅仅是能够保存页面/组件的状态这么简单,它还可以避免组件反复创建和渲染,有效提升系统性能。 总的来说,keep-alive用于保存组件的渲染状态。

2.3 keep-alive用法

  • 在动态组件中的应用
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
  <component :is="currentComponent"></component>
</keep-alive>
复制代码
  • 在vue-router中的应用
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
  <router-view></router-view>
</keep-alive>
复制代码

include 定义缓存白名单,keep-alive会缓存命中的组件; exclude 定义缓存黑名单,被命中的组件将不会被缓存; max 定义缓存组件上限,超出上限使用LRU的策略置换缓存数据。

三、源码剖析

keep-alive.js 内部另外还定义了一些 工具 函数,我们按住不表,先看它对外暴露的对象。

// src/core/components/keep-alive.js
export default {
  name: 'keep-alive',
  abstract: true, // 判断当前组件虚拟dom是否渲染成真是dom的关键

  props: {
    include: patternTypes, // 缓存白名单
    exclude: patternTypes, // 缓存黑名单
    max: [String, Number] // 缓存的组件实例数量上限
  },

  created () {
    this.cache = Object.create(null) // 缓存虚拟dom
    this.keys = [] // 缓存的虚拟dom的健集合
  },

  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 () {
    // 先省略...
  }
}
复制代码

可以看出,与我们定义组件的过程一样,先是设置组件名为 keep-alive ,其次定义了一个 abstract 属性,值为 true 。这个属性在vue的官方教程并未提及,却至关重要,后面的渲染过程会用到。 props 属性定义了keep-alive组件支持的全部参数。

keep-alive在它生命周期内定义了三个钩子函数:

  • created

    初始化两个对象分别缓存VNode(虚拟DOM)和VNode对应的健集合

  • destroyed

    删除 this.cache 中缓存的VNode实例。我们留意到,这里不是简单地将 this.cache 至为 null ,而是遍历调用 pruneCacheEntry 函数删除。

// src/core/components/keep-alive.js
function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy() // 执行组件的destory钩子函数
  }
  cache[key] = null
  remove(keys, key)
}
复制代码

删除缓存VNode还要对应执行组件实例的 destory 钩子函数

  • mounted

    mounted 这个钩子中对 includeexclude 参数进行监听,然后实时地更新(删除) this.cache 对象数据。 pruneCache 函数的核心也是去调用 pruneCacheEntry

  • render

// src/core/components/keep-alive.js
  render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot) // 找到第一个子组件对象
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) { // 存在组件参数
      // check pattern
      const name: ?string = 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: ?string = vnode.key == null // 定义组件的缓存key
        // 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
        remove(keys, key)
        keys.push(key) // 调整key排序
      } else {
        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包裹着的第一个子组件对象及其组件名;
  • 第二步:根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则执行第三步;
  • 第三步:根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 keythis.keys 中的位置(更新key的位置是实现LRU置换策略的关键),否则执行第四步;
  • 第四步:在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 的设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key)。
  • 第五步:最后并且很重要,将该组件实例的 keepAlive 属性值设置为 true 。这个在 @ 不可忽视:钩子函数 章节会再次出场。

四、重头戏:渲染

4.1 Vue的渲染过程

借一张图看下Vue渲染的整个过程:

彻底揭秘keep-alive原理
Vue的渲染是从图中的 render 阶段开始的,但keep-alive的渲染是在patch阶段

,这是构建组件树(虚拟DOM树),并将VNode转换成真正DOM节点的过程。

简单描述从 renderpatch 的过程

我们从最简单的 new Vue 开始:

import App from './App.vue'

new Vue({
  render: h => h(App),
}).$mount('#app')
复制代码
  • Vue在渲染的时候先调用原型上的 _render 函数将组件对象转化为一个VNode实例;而 _render 是通过调用 createElementcreateEmptyVNode 两个函数进行转化;
  • createElement 的转化过程会根据不同的情形选择 new VNode 或者调用 createComponent 函数做VNode实例化;
  • 完成VNode实例化后,这时候Vue调用原型上的 _update 函数把VNode渲染为真实DOM,这个过程又是通过调用 __patch__ 函数完成的(这就是pacth阶段了)

用一张图表达:

彻底揭秘keep-alive原理

4.2 keep-alive组件的渲染

我们用过keep-alive都知道,它不会生成真正的DOM节点,这是怎么做到的?

// 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 属性决定是否忽略某个组件。在keep-alive中,设置了 abstract: true ,那Vue就会跳过该组件实例。

keep-alive包裹的组件是如何使用缓存的?在 patch 阶段,会执行 createComponent 函数:

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) // 将缓存的DOM(vnode.elm)插入父元素中
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }
复制代码
  • 在首次加载被包裹组件时,由 keep-alive.js 中的 render 函数可知, vnode.componentInstance 的值是 undefinedkeepAlive 的值是 true ,因为keep-alive组件作为父组件,它的 render 函数会先于被包裹组件执行;那么就只执行到 i(vnode, false /* hydrating */) ,后面的逻辑不再执行;
  • 再次访问被包裹组件时, vnode.componentInstance 的值就是已经缓存的组件实例,那么会执行 insert(parentElm, vnode.elm, refElm) 逻辑,这样就直接把上一次的DOM插入到了父元素中。

五、不可忽视:钩子函数

5.1 只执行一次的钩子

一般的组件,每一次加载都会有完整的生命周期,即生命周期里面对应的钩子函数都会被触发,为什么被keep-alive包裹的组件却不是呢? 我们在 @ 源码剖析 章节分析到,被缓存的组件实例会为其设置 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
    ) {
      // 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)
    }
  }
  // ...
}
复制代码

可以看出,当 vnode.componentInstancekeepAlive 同时为truly值时,不再进入 $mount 过程,那 mounted 之前的所有钩子函数( beforeCreatecreatedmounted )都不再执行。

5.2 可重复的activated

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

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 */)
      }
    }
  // ...
}
复制代码

在这个钩子里面,调用了 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 函数。

参考


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Python 3面向对象编程

Python 3面向对象编程

[加]Dusty Phillips(达斯帝•菲利普斯) / 肖鹏、常贺、石琳 / 电子工业出版社 / 2015-6 / 79.00元

Python 是一种面向对象的解释型语言,面向对象是其非常重要的特性。《Python 3面向对象编程》通过Python 的数据结构、语法、设计模式,从简单到复杂,从初级到高级,一步步通过例子来展示了Python 中面向对象的概念和原则。 《Python 3面向对象编程》不是Python 的入门书籍,适合具有Python 基础经验的开发人员阅读。如果你拥有其他面向对象语言的经验,你会更容易理解......一起来看看 《Python 3面向对象编程》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具