找回密码
 会员注册
查看: 27|回复: 0

keep-alive原理剖析keep-alive原理剖析

[复制链接]

9

主题

0

回帖

28

积分

新手上路

积分
28
发表于 2024-9-19 16:50:03 | 显示全部楼层 |阅读模式
背景在vue中有一个组件叫keep-alive,它的作用其实很简单,主要是缓存:对包裹在其中的动态切换组件进行缓存。但是,它提高性能的效果到底怎样呢?基于这样的思考,在项目中,我们在一个页面分别加keep-alive与不加进行了一个对比。秉着严谨求真的精神,我们采用单一变量法,在相同的触发条件、执行环境中,触发相同次数后做对比,对比结果如下:使用keep-alive不使用keep-alive当相同组件一个使用keep-alive,一个未使用,在反复切换路由次数相同的情况下,能够发现,除去空闲时间,使用keep-alive的组件在页面的渲染、加载时间上,是要略微优于未使用的组件的。当页面内容并不复杂时,这个时间感受并不强烈,然而当组件和页面越来越复杂时,使用keep-alive带来的性能优化也就愈发明显了。接下来,我们就一起了解下,keep-alive的具体用法和运行机制到底是怎样的。正文一.keep-alive简介先放官方文档介绍,文档地址:keep-aliveAPI[1]1">propsinclude:只有名称匹配的组件才会被缓存exclude:任何名称匹配的组件都不会被缓存max:最多可以缓存多少组件实例。(一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉)用法包裹动态组件时,会缓存不活动的组件实例,而不是销毁他们。当组件在内被切换,它的activated和deactivated两个生命周期钩子函数将会被执行。简单理解就是说,我们可以把一些不常变动的组件或者需要缓存的组件用包裹起来,这样就会帮我们把组件保存在内存中,而不是直接的销毁,这样做可以保留组件的状态,以提高页面性能。了解了的用法,接下来我们一起具体分析下源码中它是如何进行性能优化、组件缓存和缓存优化处理的。二.源码解析1)渲染过程a.首次渲染vue其中一个特点就是vdom,vue普通组件模板编译的过程为:模板->AST->render()->vnode->真实Dom,此时会进入patch阶段,在函数中会将vnode转化为真实dom。而组件在初次渲染时,的vnode会视为普通组件vnode,因此一开始也会调用createComponent()函数,createComponent()会执行组件初始化函数init(), 对组件进行初始化和实例化,具体代码逻辑如下,以下代码为vue2.6.14版本vdom中patch.js[2]:functioncreateComponent(vnode,insertedVnodeQueue,parentElm,refElm){vari=vnode.data;if(isDef(i)){//isReactivated用来判断组件是否缓存varisReactivated=isDef(vnode.componentInstance)&i.keepAlive;if(isDef(i=i.hook)&isDef(i=i.init)){//执行组件初始化的内部钩子init()i(vnode,false/*hydrating*/);}//aftercallingtheinithook,ifthevnodeisachildcomponent//itshould'vecreatedachildinstanceandmountedit.//thechildcomponentalsohassettheplaceholdervnode'selm.//inthatcasewecanjustreturntheelementandbedone.if(isDef(vnode.componentInstance)){initComponent(vnode,insertedVnodeQueue);//将真实dom添加到父节点,insert操作domapiinsert(parentElm,vnode.elm,refElm);if(isTrue(isReactivated)){reactivateComponent(vnode,insertedVnodeQueue,parentElm,refElm);}returntrue}}}首次渲染时,组件 vnode 没有 componentInstance属性,vnode.data.keepAlive 也没有值,所以会调用 createComponentInstanceForVnode() 将组件进行实例化并将组件实例赋值给 vnode 的componentInstance 属性,其中createComponentInstanceForVnode()是组件实例化的过程,一系列选项合并、初始化事件、生命周期等初始化操作,最后执行组件实例的 $mount方法进行实例挂载。具体代码逻辑如下,代码参考地址create-component.js[3]://inlinehookstobeinvokedoncomponentVNodesduringpatchvarcomponentVNodeHooks={//组件vnode初始化init(vnode:VNodeWithData,hydrating:boolean):?boolean{if(vnode.componentInstance&!vnode.componentInstance._isDestroyed&vnode.data.keepAlive){//kept-alivecomponents,treatasapatchconstmountedNode:any=vnode//workaroundflowcomponentVNodeHooks.prepatch(mountedNode,mountedNode)}else{//第一次运行时,vnode.componentInstance不存在,vnode.data.keepAlive不存在。//将组件实例化,并赋值给vnode的componentInstance属性。//createComponentInstanceForVnode()是组件实例化的过程,一系列选项合并,初始化事件,生命周期等初始化操作。constchild=vnode.componentInstance=createComponentInstanceForVnode(vnode,activeInstance)//进行挂载child.$mount(hydrating?vnode.elm:undefined,hydrating)}},//prepatch是patch过程的核心步骤prepatch:functionprepatch(oldVnode,vnode){...},insert:functioninsert(vnode){...},destroy:functiondestroy(vnode){...}};缓存vnode挂载 $mount 阶段会调用 vm._render() 函数,最终会调用组件选项中的 render() 函数进行渲染。由于 是一个内置组件,因此也拥有自己的render() 函数、created及mounted方法。组件的定义位于源码的 src/core/components/keep-alive.js 文件中,以下代码则为组件的核心代码,代码参考地址:keep-alive.js[4]我们先从created钩子开始进行分析:createdcreated(){//缓存对象cache及key值数组初始化this.cache=Object.create(null)this.keys=[]},this.cache是一个对象,用来存储需要缓存的组件,对象的key值则为对应缓存组件的key。这个key值是一个唯一的拼接字符串,value为包含组件名称、组件tag、组件实例的一个对象。this.keys是一个数组,用来存储每个需要缓存的组件的key,即对应this.cache对象中的键值。render作为缓存组件较核心的代码,render()大致做了以下几项处理:a.获取slot中第一个组件节点。b.获取该组件节点的名称,用组件name与 include、exclude 中的匹配规则匹配,如果与 include 规则不匹配或者与 exclude 规则匹配,则不缓存该组件,直接返回该组件的 vnode,否则走下一步缓存。c.根据组件key值在this.cache对象中寻找是否有该值,如果有则表示该组件有缓存,直接从缓存中取 vnode 的组件实例,并重新调整该组件key的顺序,删掉并重新将其放置this.keys最后;如果没有,则将该组件实例及key值存储,用于mounted/updated阶段组件缓存处理。d.为缓存组件打上标记,设置keepAlive为true,并返回vnode。//在渲染阶段,进行缓存的存/取render(){//首先拿到keep-alive下插槽的默认值(keep-alive包裹的组件)constslot=this.$slots.default//获取第一个vnode节点constvnode:VNode=getFirstComponentChild(slot)//拿到第一个子组件实例constcomponentOptions:?VNodeComponentOptions=vnode&vnode.componentOptionsif(componentOptions){//checkpatternconstname:?string=getComponentName(componentOptions)const{include,exclude}=thisif(//notincluded//配置include且不匹配或配置了exclude且匹配,直接返回;否则,走下面的缓存逻辑(include&(!name||!matches(include,name)))||//excluded(exclude&name&matches(exclude,name))){returnvnode}const{cache,keys}=thisconstkey:?string=vnode.key==null//sameconstructormaygetregisteredasdifferentlocalcomponents//socidaloneisnotenough(#3269)//获取本地组件唯一key?componentOptions.Ctor.cid+(componentOptions.tag?`:{componentOptions.tag}`:''):vnode.keyif(cache[key]){//缓存过的组件直接进行读取,且前置当前读取组件key顺序vnode.componentInstance=cache[key].componentInstance//makecurrentkeyfreshest//使用LRU最近最少缓存策略,将命中的key从缓存数组中删除,并将当前最新key存入缓存数组的末尾remove(keys,key)//将当前组件名重新存入数组最末端keys.push(key)}else{//delaysettingthecacheuntilupdate//用于mounted/updated阶段组件的首次缓存this.vnodeToCache=vnodethis.keyToCache=key}//为缓存组件打上标记vnode.data.keepAlive=true}returnvnode||(slot&slot[0])}mounted/updated在mounted和updated钩子函数中首先都会执行cacheVNode函数,该函数主要做了以下处理:首先判断vnodeToCache是否存在(即是否在render中进行过实例的暂时缓存),存在则以cid和tag组成的唯一标识作为该组件的key键,以包含组件名称、组件tag、组件实例的对象为value值,将其存入this.cache中,并将key存入this.keys中。同时判断this.keys中缓存组件数量是否超过了设置的最大缓存数量值this.max,超过则删掉第一个缓存组件(即根据LRU置换策略删除最近最久未使用的实例,即key[0])。在mounted钩子中,还会监测 include 和 exclude的变化,若发生了变化,即表示缓存的组件的规则或者不需要缓存的组件的规则发生了变化,就执行pruneCache函数,pruneCache()主要对不符合include规则和符合exclude规则的组件,进行移除cache缓存对象中对应组件的操作。//updatedmethods:{cacheVNode(){const{cache,keys,vnodeToCache,keyToCache}=thisif(vnodeToCache){//如果vnodeToCache存在,则将该组件缓存到cache对象中,以key值为键,以cid和tag组成的唯一标识作为//该组件的key,以包含组件名称、组件tag、组件实例的对象为value值const{tag,componentInstance,componentOptions}=vnodeToCachecache[keyToCache]={name:getComponentName(componentOptions),tag,componentInstance,}keys.push(keyToCache)//pruneoldestentry//与max进行对比,超过则删除最少使用即下标最前的那个组件if(this.max&keys.length>parseInt(this.max)){pruneCacheEntry(cache,keys[0],keys,this._vnode)}this.vnodeToCache=null}}},...updated(){this.cacheVNode()},//mountedmounted(){this.cacheVNode()this.$watch('include',val=>{pruneCache(this,name=>matches(val,name))})this.$watch('exclude',val=>{pruneCache(this,name=>!matches(val,name))})},...//对不符合include规则和符合exclude规则的组件,进行移除cache缓存对象中对应组件的操作functionpruneCache(keepAliveInstance:any,filter:Function){const{cache,keys,_vnode}=keepAliveInstancefor(constkeyincache){constentry:?CacheEntry=cache[key]if(entry){constname:?string=entry.nameif(name&!filter(name)){pruneCacheEntry(cache,key,keys,_vnode)}}}}destroyed当组件被销毁时,此时会调用destroyed钩子函数,在该钩子函数里会遍历this.cache对象,然后将那些被缓存的并且当前没有处于被渲染状态的组件都销毁掉并将其从this.cache对象中删除。//销毁组件,且移除缓存数组和key数组中对应的数据functionpruneCacheEntry(cache:CacheEntryMap,key:string,keys:Array,current?:VNode){constentry:?CacheEntry=cache[key]if(entry&(!current||entry.tag!==current.tag)){entry.componentInstance.$destroy()}cache[key]=nullremove(keys,key)}...destroyed(){for(constkeyinthis.cache){pruneCacheEntry(this.cache,key,this.keys)}},缓存真实dom以上渲染过程为调用createComponent()函数执行组件的初始化等操作(即init())的过程,当初始化完成后,则会执行initComponent(),将真实dom添加到父节点,同时将真实的dom保存在vnode中,代码参考地址:patch.js[5],具体代码如下:functioninitComponent(vnode,insertedVnodeQueue){if(isDef(vnode.data.pendingInsert)){insertedVnodeQueue.push.apply(insertedVnodeQueue,vnode.data.pendingInsert);vnode.data.pendingInsert=null;}//保存真实dom节点到vnodevnode.elm=vnode.componentInstance.$el...}b.缓存渲染当数据发送变化,在patch的过程中会执行patchVnode的逻辑,它会对比新旧vnode节点,甚至对比它们的子节点去做更新逻辑,但是对于组件vnode而言,是没有children的,而对于组件而言,patchVnode在做各种diff之前,会先执行prepatch的钩子函数,prepatch核心逻辑就是执行updateChildComponent方法,代码参考地址:patch.js[6]。exportfunctionupdateChildComponent(vm:Component,propsData:?Object,listeners:?Object,parentVnode:MountedComponentVNode,renderChildren:?Array){consthasChildren=!!(renderChildren||vm.$options._renderChildren||parentVnode.data.scopedSlots||vm.$scopedSlots!==emptyObject)//...if(hasChildren){vm.$slots=resolveSlots(renderChildren,parentVnode.context)vm.$forceUpdate()}}updateChildComponent方法主要是去更新组件实例的一些属性,这里我们重点关注一下slot部分,由于组件本质上支持了slot,所以它执行prepatch的时候,需要对自己的children,也就是这些slots做重新解析,并触发组件实例$forceUpdate逻辑,也就是重新执行的render方法,这个时候如果它包裹的第一个组件vnode命中缓存,则直接返回缓存中的vnode.componentInstance,接着又会执行patch过程,再次执行到createComponent方法。但这个时候isReactivated为true,并且在执行init钩子函数的时候不会再执行组件的mount过程,这也就是被包裹的组件在有缓存的时候就不会在执行组件的created、mounted等钩子函数的原因了。同时,变化的dom也会被更新至页面中。2)abstract(抽象组件)最开始设置的abstract属性值为true,代表是一个抽象组件。vue官方文档中说:是一个抽象组件:它自身不会渲染一个DOM元素,也不会出现在父组件链中。exportdefault{name:'keep-alive',abstract:true,props:{...}组件一旦被缓存,那么再次渲染时就不会执行created、mounted等钩子函数。但是有些业务场景需要在被缓存的组件重新渲染时做一些事情,vue则提供了activated和deactivated钩子函数。vue在初始化生命周期的时候,为组件实例建立父子关系时会根据abstract属性决定是否忽略某个组件,组件中设置了abstract:true,vue会跳过该组件实例。代码参考地址:lifecycle.js[7],具体代码如下:exportfunctioninitLifecycle(vm:Component){constoptions=vm.$options//locatefirstnon-abstractparentletparent=options.parent//只有abstract不存在或为false时才会走生命周期中的逻辑if(parent&!options.abstract){while(parent.$options.abstract&parent.$parent){parent=parent.$parent}parent.$children.push(vm)}...}三.缓存策略上面介绍了实现的具体原理,由于  中的缓存优化遵循LRU原则,所以我们也了解下缓存淘汰策略的相关介绍。由于缓存空间是有限的,不能无限制的进行数据存储,当存储容量达到一个阀值时,就会造成内存溢出,因此在进行数据缓存时,就要根据情况对缓存进行优化,清除一些可能不会再用到的数据。根据缓存淘汰机制的不同,常用的有以下三种:1.FIFO(fisrt-in-fisrt-out)-先进先出策略。我们通过记录数据使用的时间,当缓存大小即将溢出时,优先清除离当前时间最远的数据。2.LRU(least-recently-used)-最近最少使用策略。以时间作为参考,如果数据最近被访问过,那么将来被访问的几率会更高,如果以一个数组去记录数据,当有一数据被访问时,该数据会被移动到数组的末尾,表明最近被使用过,当缓存溢出时,会删除数组的头部数据,即将最不频繁使用的数据移除。(keep-alive采用的处理方式) 3.LFU(least-frequently-used)-计数最少策略。以次数作为参考,用次数去标记数据使用频率,次数最少的会在缓存溢出时被淘汰。对于我们平时业务场景中,作为提高性能最常用的方式-缓存,根据不同场景选择相应的缓存策略,也会大大提高系统的性能。四.应用场景在平时的业务,我们经常会有从一个页面跳到另一个页面,然后返回上一页时需要缓存状态的情况。基于这样的场景,就可以考虑使用缓存组件。通过在中包裹路由组件,实现在路由来回切换时,页面通过走缓存而提高性能。对于内容比较长比较多的页面,多个组件之间来回的切换,频繁加载也是很耗时的,此时也可以通过使用将多个类似组件进行缓存,来提高性能。总结本文首先简单介绍了的概念和用法。其次从的渲染开始,到组件内部mounted、render函数的缓存核心逻辑的解读,再到与普通组件生命周期的区别对比,再通过对缓存策略的解读延伸到目前常见的缓存策略,使我们更深入的了解keep-alive组件,同时更合理的应用到对应的业务场景中。 组件是抽象组件,在对应父子关系时会跳过抽象组件,它只对包裹的子组件做处理。Vue 内部将DOM节点抽象为一个个的VNode节点,组件的缓存也是基于VNode节点而不是直接存储DOM结构。根据LRU策略缓存组件 VNode,最后在 render 时返回子组件的 VNode。缓存渲染过程会更新  插槽,重新再 render 一次,从缓存中读取之前的组件 VNode 实现状态缓存。它将满足条件(include与exclude)的组件在cache对象中缓存起来,在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染。了解了的实现原理,我们也应该注意到,虽然可以通过缓存的方式提高页面加载性能,但这也是建立在消耗内存的基础上,当缓存数量达到一定量时,效果也可能适得其反,所以对于keep-alive的使用,也是要注意使用时机和方式的。最后我们又简单介绍了下缓存策略,以及在真实项目中的应用场景。至此,的介绍就告一段落了。作者介绍李馨馨:日常热衷中医养生的佛系girl~参考资料[1]keep-aliveAPI:https://v2.cn.vuejs.org/v2/api/?#keep-alive[2]patch.js:https://github.com/vuejs/vue/blob/612fb89547711cacb030a3893a0065b785802860/src/core/vdom/patch.js#L210[3]create-component.js:https://github.com/vuejs/vue/blob/v2.6.14/src/core/vdom/create-component.js[4]keep-alive.js:https://github.com/vuejs/vue/blob/v2.6.14/src/core/components/keep-alive.js[5]patch.js:https://github.com/vuejs/vue/blob/v2.6.14/src/core/vdom/patch.js[6]patch.js:https://github.com/vuejs/vue/blob/612fb89547711cacb030a3893a0065b785802860/src/core/instance/lifecycle.js#L215[7]lifecycle.js:https://github.com/vuejs/vue/blob/v2.6.14/src/core/instance/lifecycle.js
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-26 23:50 , Processed in 0.390410 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表