彻底揭秘keep-alive原理(小结)


Posted in Javascript onMay 05, 2019

一、前言

原文链接: https://github.com/qiudongwei/blog/issues/4

本文介绍的内容包括:

  • 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 函数:

// src/core/vdom/patch.js
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 钩子:

// 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 */)
   }
  }
 // ...
}

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

参考

 Vue技术揭秘|keep-alive

Vue源码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JS 添加千分位与去掉千分位的示例
Jul 11 Javascript
javascript简单实现表格行间隔显示颜色并高亮显示
Nov 29 Javascript
JS实现控制表格只显示行边框或者只显示列边框的方法
Mar 31 Javascript
分享一些常用的jQuery动画事件和动画函数
Nov 27 Javascript
js实现小窗口拖拽效果
Dec 03 Javascript
数组Array的一些方法(总结)
Feb 17 Javascript
Vue实现百度下拉提示搜索功能
Jun 21 Javascript
Vue实现美团app的影院推荐选座功能【推荐】
Aug 29 Javascript
详解js获取video任意时间的画面截图
Apr 17 Javascript
Vue将页面导出为图片或者PDF
Aug 17 Javascript
vue中使用rem布局代码详解
Oct 30 Javascript
ES6中的Javascript解构的实现
Oct 30 Javascript
angular4+百分比进度显示插件用法示例
May 05 #Javascript
vuejs数据超出单行显示更多,点击展开剩余数据实例
May 05 #Javascript
Vue+Express实现登录状态权限验证的示例代码
May 05 #Javascript
微信小程序实现发送模板消息功能示例【通过openid推送消息给用户】
May 05 #Javascript
浅谈Node 异步IO和事件循环
May 05 #Javascript
vue的列表交错过渡实现代码示例
May 05 #Javascript
微信小程序上传多图到服务器并获取返回的路径
May 05 #Javascript
You might like
PHP转换文件夹下所有文件编码的实现代码
2013/06/06 PHP
对PHP依赖注入的理解实例分析
2016/10/09 PHP
PHP文件上传小程序 适合初学者学习!
2019/05/23 PHP
Javascript实例教程(19) 使用HoTMetal(5)
2006/12/23 Javascript
Jquery 基础学习笔记
2009/05/29 Javascript
9款2014最热门jQuery实用特效推荐
2014/12/07 Javascript
js密码强度校验
2015/11/10 Javascript
浅谈JavaScript的内置对象和浏览器对象
2016/06/03 Javascript
JS实现的手机端精简幻灯片效果
2016/09/05 Javascript
Nodejs 发送Post请求功能(发短信验证码例子)
2017/02/09 NodeJs
JavaScript设置名字输入不合法的实现方法
2017/05/23 Javascript
vue用addRoutes实现动态路由的示例
2017/09/15 Javascript
Web开发使用Angular实现用户密码强度判别的方法
2017/09/27 Javascript
webuploader分片上传的实现代码(前后端分离)
2018/09/10 Javascript
小程序实现左右来回滚动字幕效果
2018/12/28 Javascript
vue请求本地自己编写的json文件的方法
2019/04/25 Javascript
详解在Angular4中使用ng2-baidu-map的方法
2019/06/19 Javascript
Vue使用CDN引用项目组件,减少项目体积的步骤
2020/10/30 Javascript
[00:32]2018DOTA2亚洲邀请赛出场——VP
2018/04/04 DOTA
python命令行参数sys.argv使用示例
2014/01/28 Python
Python 装饰器实现DRY(不重复代码)原则
2018/03/05 Python
pycharm打开命令行或Terminal的方法
2019/01/16 Python
springboot配置文件抽离 git管理统 配置中心详解
2019/09/02 Python
浅析python,PyCharm,Anaconda三者之间的关系
2019/11/27 Python
python利用os模块编写文件复制功能——copy()函数用法
2020/07/13 Python
伦敦哈德森鞋:Hudson Shoes
2018/02/06 全球购物
薇姿法国官网:Vichy法国
2021/01/28 全球购物
学生周末长期请假条
2014/02/15 职场文书
企业演讲比赛主持词
2014/03/18 职场文书
安全技术说明书
2014/05/09 职场文书
师德师风自查总结
2014/10/14 职场文书
欠条格式范本
2015/07/03 职场文书
清明节主题班会
2015/08/14 职场文书
导游词之京东大峡谷旅游区
2019/10/29 职场文书
PyQt5 QThread倒计时功能的实现代码
2021/04/02 Python
JavaScript 定时器详情
2021/11/11 Javascript