keep-alive是Vue.js的一个内置组件。它可以将不活跃的组件实例保存在内存中,我们来探究一下它的源码实现。先回顾一下使用方法,举个栗子alive>exportdefault{data(){return{isShow:true}},methods:{handleClick(){this.isShow=!this.isShow;}}}当点击按钮时,两个组件会切换,但是此时会缓存两个组件的状态,例如:组件中有一个input标签,那么input标签中的内容不会因组件切换而改变而消失。属性支持keep-alive组件提供了include和exclude两个属性来允许组件有条件地缓存,这两个属性都可以用逗号分隔的字符串、正则表达式或数组来表示。例如:缓存名为a的组件。排除缓存名为a的组件。当然props也定义了max,可以让我们指定缓存大小。keep-alive源码实现说完keep-alive组件的使用,我们再从源码的角度来看一下keep-alive组件是如何实现组件缓存的。创建和销毁阶段首先看看keep-alive创建和销毁阶段做了什么:created(){/*cacheobject*/this.cache=Object.create(null)},destroyed(){for(constkeyinthis.cache){pruneCacheEntry(this.cache[key])}},在keep-alive的创建阶段:createdhook会创建一个缓存对象来保存vnode节点。在销毁阶段:被销毁的钩子会调用pruneCacheEntry方法清除缓存中的所有组件实例。pruneCacheEntry方法的源码实现/*销毁vnode对应的组件实例(Vue实例)*/functionpruneCacheEntry(vnode:?VNode){if(vnode){vnode.componentInstance.$destroy()}}因为keep-alive会销毁组件,它保存在内存中,不会被销毁或重新创建,所以不会再次调用组件的created等方法,所以keep-alive提供了两个生命钩子,分别是activated和deactivated。使用这两个生命钩子可以知道当前组件是否处于活动状态。(后面会看源码是怎么实现的)renderingstagerender(){/*获取slot中的第一个组件*/constvnode:VNode=getFirstComponentChild(this.$slots.default)constcomponentOptions:?VNodeComponentOptions=vnode&&vnode.componentOptionsif(componentOptions){//获取组件的名称,首先获取组件的name字段,否则为组件的tagconstname:?string=getComponentName(componentOptions)//不需要缓存,返回vnodeif(name&&((this.include&&!matches(this.include,name))||(this.exclude&&matches(this.exclude,name)))){returnvnode}constkey:?字符串=vnode.key==null?componentOptions.Ctor.cid+(componentOptions.tag?`::${componentOptions.tag}`:''):vnode.keyif(this.cache[key]){//如果有缓存,取缓存的组件实例vnode.componentInstance=this.cache[key].componentInstance}else{//如果没有缓存,则创建缓存this.cache[key]=vnode//创建缓存时//如果配置了max并且缓存的长度超过了this.max//则从缓存中删除第一个if(this.max&&keys.length>parseInt(this.max)){pruneCacheEntry(this.cache,keys[0],keys,this._vnode)}}//keepAliveflagvnode.data.keepAlive=true}returnvnode}render做了以下事情:通过getFirstComponentChild获取第一个子组件,获取组件如果有组件名,则直接使用组件名,否则使用标签通过include和exclude属性匹配名称。如果匹配不成功(说明不需要缓存),匹配成功则直接返回vnode。如果匹配成功,则尝试获取缓存的组件实例。如果组件没有被缓存,如果组件缓存超过最大值,将删除第一个缓存的名称匹配方法(检查是否为逗号分隔字符串或正则模式)/*检查名称是否匹配*/functionmatches(pattern:string|RegExp,name:string):boolean{if(typeofpattern==='string'){/*字符串大小写,例如a,b,c*/returnpattern.split(',').indexOf(name)>-1}elseif(isRegExp(pattern)){/*regular*/returnpattern.test(name)}/*istanbulignorenext*/returnfalse}如果include和exclude怎么办中间有修改吗?作者通过watch监听include和exclude,变化时调用pruneCache修改cache缓存中的缓存数据watch:{/*监听include和exclude,缓存被修改时修改*/include(val:string|RegExp){pruneCache(this.cache,this._vnode,name=>matches(val,name))},exclude(val:string|RegExp){pruneCache(this.cache,this._vnode,name=>!matches(val,name))}}那么pruneCache是??做什么的呢?//修补缓存函数pruneCache(cache:VNodeCache,current:VNode,filter:Function){for(constkeyincache){//尝试获取缓存中的vnodeconstcachedNode:?VNode=cache[key]if(cachedNode){constname:?string=getComponentName(cachedNode.componentOptions)if(name&&!filter(name)){//重新过滤组件if(cachedNode!==current){//不在当前_vnodepruneCacheEntry(cachedNode)//调用组件实例的destroy方法}cache[key]=null//移除缓存}}}}pruneCache方法遍历缓存中的所有项,如果不满足规则,该节点将被destroyed,缓存会被移除进阶回顾下载源码,exportdefault{name:'keep-alive,abstract:true,props:{insrc/core/components/keep-alive.jsclude:patternTypes,exclude:patternTypes,max:[String,Number]},created(){this.cache=Object.create(null)this.keys=[]},destroyed(){for(constkeyinthis.缓存){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(){constslot=this.$slots.defaultconstvnode:VNode=getFirstComponentChild(slot)constcomponentOptions:?VNodeComponentOptions=vnode&&vnode.componentOptionsif(componentOptions){constname:?string=getComponentName(componentOptions)const{include,exclude}=thisif((include&&(!name||!匹配(包括,名称)))||(排除&&名称&&匹配(排除,名称))){返回vnode}const{cache,keys}=这个constkey:?string=vnode.key==null?componentOptions.Ctor.cid+(componentOptions.tag?`::${componentOptions.tag}`:''):vnode.keyif(cache[key]){vnode.componentInstance=cache[key].componentInstanceremove(keys,key)keys.push(key)}else{cache[key]=vnodekeys.push(key)if(this.max&&keys.length>parseInt(this.max)){pruneCacheEntry(cache,keys[0],键,this._vnode)}}vnode.data.keepAlive=true}returnvnode||(slot&&slot[0])}}没有注释,现在大部分应该都能看懂了吧?对了,属性abstract,如果abstract为true,表示该组件是一个抽象组件,不会渲染到真实的DOM中,也不会出现在父组件链中那么为什么当组件有缓存时,组件的created、mounted等钩子函数没有再次执行呢?constcomponentVNodeHooks={init(vnode:VNodeWithData,hydrating:boolean):?boolean{//进入这个逻辑.prepatch(mountedNode,mountedNode)}else{constchild=vnode.componentInstance=createComponentInstanceForVnode(vnode,activeInstance)child.$mount(hydrating?vnode.elm:undefined,hydrating)}},//...}见上文如果代码满足,vnode.componentInstance&&!vnode.componentInstance._isDestroyed&&vnode.data.keepAlive的逻辑不会执行$mount操作,而是执行prepatch。那么prepatch到底做了什么?//所有不重要的内容都省略了...prepatch(oldVnode:MountedComponentVNode,vnode:MountedComponentVNode){//这个方法会被执行updateChildComponent(//...)},//...主要是执行updateChildComponent函数。functionupdateChildComponent(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()}}keep-alivecomponent本质上它是通过slot实现的,所以在执行prepatch的时候,hasChildren=true会触发组件的$forceUpdate逻辑,即重新执行keep-alive的render方法。但是根据上面提到的render方法的源码,会稍微发现Cache。然后,介绍了的实现原理。最后,喜欢原创也不容易。欢迎关注公众号《前端进阶教程》,认真学习前端,共同进步。