You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
exportdefault{name: 'keep-alive',abstract: true,props: {include: patternTypes,exclude: patternTypes,max: [String,Number]},created(){this.cache=Object.create(null)this.keys=[]},destroyed(){for(constkeyinthis.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(){constslot=this.$slots.defaultconstvnode: VNode=getFirstComponentChild(slot)// 获取第一个组件节点constcomponentOptions: ?VNodeComponentOptions=vnode&&vnode.componentOptionsif(componentOptions){// check patternconstname: ?string=getComponentName(componentOptions)const{ include, exclude }=thisif(// not included(include&&(!name||!matches(include,name)))||// excluded(exclude&&name&&matches(exclude,name))){returnvnode}// 缓存vnodeconst{ cache, keys }=thisconstkey: ?string=vnode.key==null// same constructor may get registered as different local components// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid+(componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.keyif(cache[key]){vnode.componentInstance=cache[key].componentInstance// make current key freshestremove(keys,key)keys.push(key)}else{cache[key]=vnodekeys.push(key)// prune oldest entryif(this.max&&keys.length>parseInt(this.max)){pruneCacheEntry(cache,keys[0],keys,this._vnode)}}vnode.data.keepAlive=true}returnvnode||(slot&&slot[0])}}
functionmatches(pattern: string|RegExp|Array<string>,name: string): boolean{if(Array.isArray(pattern)){returnpattern.indexOf(name)>-1}elseif(typeofpattern==='string'){returnpattern.split(',').indexOf(name)>-1}elseif(isRegExp(pattern)){returnpattern.test(name)}/* istanbul ignore next */returnfalse}
接下来就是对拿到的子组件的vnode进行缓存操作:
// 缓存vnodeconst{ cache, keys }=thisconstkey: ?string=vnode.key==null// same constructor may get registered as different local components// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid+(componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.keyif(cache[key]){vnode.componentInstance=cache[key].componentInstance// make current key freshestremove(keys,key)keys.push(key)}else{cache[key]=vnodekeys.push(key)// prune oldest entryif(this.max&&keys.length>parseInt(this.max)){pruneCacheEntry(cache,keys[0],keys,this._vnode)}}
init(vnode: VNodeWithData,hydrating: boolean): ?boolean{if(vnode.componentInstance&&!vnode.componentInstance._isDestroyed&&vnode.data.keepAlive){// kept-alive components, treat as a patchconstmountedNode: any=vnode// work around flowcomponentVNodeHooks.prepatch(mountedNode,mountedNode)}else{// 创建组件实例constchild=vnode.componentInstance=createComponentInstanceForVnode(vnode,activeInstance// 组件占位符所在的vm)child.$mount(hydrating ? vnode.elm : undefined,hydrating)}}
if(vnode.componentInstance&&!vnode.componentInstance._isDestroyed&&vnode.data.keepAlive){// kept-alive components, treat as a patchconstmountedNode: any=vnode// work around flowcomponentVNodeHooks.prepatch(mountedNode,mountedNode)}
// src/core/observer/scheduler.jsexportfunctionqueueActivatedComponent(vm: Component){// setting _inactive to false here so that a render function can// rely on checking whether it's in an inactive tree (e.g. router-view)vm._inactive=falseactivatedChildren.push(vm)}functioncallActivatedHooks(queue){for(leti=0;i<queue.length;i++){queue[i]._inactive=trueactivateChildComponent(queue[i],true/* true */)}}
对于deactivated钩子,会在组件节点的destory钩子中进行处理:
destroy(vnode: MountedComponentVNode){const{ componentInstance }=vnodeif(!componentInstance._isDestroyed){if(!vnode.data.keepAlive){componentInstance.$destroy()}else{deactivateChildComponent(componentInstance,true/* direct */)}}}
当我们使用Vue的动态组件或者路由切换组件时,如果想要保存之前显示组件的状态,可以利用
keep-alive
内置组件包裹。现在通过源码来看看它的实现。组件源码
keep-alive
是Vue实现的内置组件,它和我们手写的组件一样有自己的组件配置,它定义在src/core/components/keep-alive.js
:这个组件和我们平时写的组件不同的是多了一个
abstract: true
,表示这是一个抽象组件。抽象组件的实例是不会维护它的父子关系链的,在initLifecycle
中:该组件定义了三个
include
,exclude
和max
三个props,分别表示该缓存的组件,和不该缓存的组件以及最多缓存个数。组件的created
钩子定义了cache和keys分别表示缓存的数据和key。再来看看render函数:因为
keep-alive
组件可以缓存它的第一个子组件实例,所以要通过slot
获取第一个组件节点的vnode。这段代码是拿到组件的名称判断它是否需要进行缓存。
matches
函数主要是对字符串,数组和正则表达式几种情况的匹配处理:接下来就是对拿到的子组件的vnode进行缓存操作:
主要的逻辑就是根据子组件的
key
是否命中缓存。如果命中缓存,则在缓存中获取vnode对应的实例,这样之前组件的状态就保留了。然后在把key
移动到末尾,这样的操作是为了让keys
的最后一个元素永远是最近使用的,对应第一个元素就是最久远的。如果没有命中缓存,则把vnode存进缓存数组,然后还要判断缓存的组件个数是否超过了限制,超过要删除第一个缓存的元素,这就是keys
数组的作用。来看下pruneCacheEntry
方法:删除的条件就是存在缓存并且不是当前渲染的节点。如果满足,则调用组件实例的
$destroy
方法进行卸载,然后删除对应的缓存和keys。在render
函数最后,在vnode上标记这是一个被keep-alive
缓存的组件:因为
keep-alive
组件是一个抽象组件,所以在最后返回的是第一个子组件的vnode。于是,keep-alive
组件实例的patch过程其实是对包裹的子组件进行操作。另外,
keep-alive
组件在挂载后还监听了include
和exclude
,当它们变化时要重新处理缓存,把不匹配的缓存调用pruneCacheEntry
删除掉:对应的
pruneCache
方法就是循环cache
,判断它是否仍需要进行缓存:组件渲染
先来看下一个例子:
利用
component
动态组件的方式来切换A和B组件,并用keep-alive
进行缓存对组件进行缓存。首先会调用Vue实例的patch
方法进行更新,首次渲染的vnode会调用createElm
方法创建DOM,在该方法里面会先创建div节点的DOM,然后子vnode递归调用该函数进行创建。在创建真实DOM的过程,如果遇到的是组件节点,会调用
createComponent
方法进行处理,从而创建组件对应的DOM。所以,当遇到keep-alive
组件节点是就会调用该方法:这个方法会首先执行组件vnode的
init
钩子:对于首次渲染,会创建组件实例然后调用实例的
$mount
方法进行挂载,把组件对应的DOM插入到对应的占位vnode位置。在组件的挂载过程中,就会执行组件的render
方法并进行patch
,这个时候就会执行keep-alive
组件的render
函数。执行完后会把A组件的vnode存进cache
中,然后返回A组件的vnode并调用patch
方法。在对A组件进行
patch
的过程,又会回到createComponent
方法创建A组件对应的实例和获取对应的DOM。这样走下来,patch
的过程比不用keep-alive
组件是多了一步,但是结果是一样的。当我们点击按钮切换到B组件是,因为响应式数据变化会触发当前实例的渲染
watcher
,也就是会重新执行render
和patch
。在patch
过程,然后新老的vnode是同个节点时,会调用patchVnode
方法进行比对,如果是组件节点的话还会先调用prepatch
钩子:keep-alive
组件就是在会在patch
调用该钩子,然后调用updateChildComponent
方法更新slot
等属性:在更新完
slot
后会调用vm.$forceUpdate
强制刷新,也就是会重新执行keep-alive
组件的render
并进行patch
。假设我们是点击了按钮两次,也就是说A组件被缓存了然后又从B切回A,这时候的A组件会命中缓存,并且组件的实例是从缓存中取:然后在对A组件调用
patch
过程中,发现它是和oldVnode不是同一个节点,所以调用createComponent
方法创建A组件的DOM。这个时候走到组件节点的init
钩子的时候会命中下面逻辑:因为我们已经从
keep-alive
的缓存中取出组件的实例,所以不会重新去新建一个组件的实例,从而组件的状态得以维持。因为vnode.elm
保存了之前的DOM,所以直接插入到对应的位置即可:生命周期
从上面可以知道命中
keep-alive
缓存的组件在执行init
钩子的时候不会去重新新建一个实例,所以激活的时候created
等初始化钩子就不会再调用,但是为了满足业务需求,Vue在激活组件的时候加入了activated
和deactivated
钩子。在
patch
过程的最后面,也就是在vnode对应的DOM插入到父DOM后,会执行invokeInsertHook
函数执行在patch
过程中所有的insert
钩子函数。对于组件节点,会执行它的insert
钩子:当组件实例的
_isMounted
为false
,也就是还没标记挂载时调用mounted
钩子函数。但组件的父环境还没挂载时,会调用activateChildComponent
执行组件以及子组件的actived
钩子:其中
vm._inactive
是为了让一个实例的钩子函数只调用一次。当父环境已经挂载的情况,会调用queueActivatedComponent
方法把自身组件实例放到一个数组,然后在nextTick
执行activateChildComponent
方法:对于
deactivated
钩子,会在组件节点的destory
钩子中进行处理:如果不是
keep-alive
下的组件直接调用实例的$destory
方法。否则调用deactivateChildComponent
方法执行组件以及子组件的deactivated
钩子:总结
keep-alive
的原理就是在render
函数中把默认插槽的第一个组件vnode进行缓存,然后返回这个vnode。在组件进行patch
过程中会调用prepatch
钩子,更新slot
内容后再执行渲染watcher
,然后重新执行render
函数。这个时候如果命中缓存,把缓存中的vnode对应实例直接赋值,这样在slot
组件下次调用init
钩子的时候跳过了新建实例的步骤,而是拿到原来的DOM直接插入。The text was updated successfully, but these errors were encountered: