組件的 keep-alive 簡介
本篇文章,我們來講一下keep-alive
的實現。
Vue
中,有三個內置的抽象組件,分別是keep-alive
、transition
和transition-group
,
它們都有一個共同的特點,就是自身不會渲染一個DOM元素,也不會出現在父組件鏈中。
keep-alive
的作用,是包裹動態組件時,會緩存不活動的組件實例,而不是銷毀它們。具體的用法見這裏。
該組件的定義,是在src/core/components/keep-alive.js
文件中。
它會在Vue
初始化時,添加在Vue.options.components
上,所以在所有的組件中,都可以直接只用它。
直接看代碼:
export default {
name: ‘keep-alive‘,
abstract: true,
props: {
...
},
created () {
this.cache = Object.create(null)
},
destroyed () {
...
},
watch: {
...
},
render () {
...
}
}
name
不用多說,abstract: true
這個條件我們自己定義組件時通常不會用,
它是用來標識當前的組件是一個抽象組件,它自身不會渲染一個真實的DOM元素。
比如在創建兩個vm
實例之間的父子關系時,會跳過抽象組件的實例:
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
props
表示我們可以傳入include
來匹配哪些組件可以緩存,exclude
來匹配哪些組件不緩存。
created
鉤子函數調用時,會創建一個this.cache
對象用於緩存它的子組件。
destroyed
表示keep-alive
被銷毀時,會同時銷毀它緩存的組件,並調用deactivated
鉤子函數。
function pruneCacheEntry (vnode: ?VNode) {
if (vnode) {
if (!vnode.componentInstance._inactive) {
callHook(vnode.componentInstance, ‘deactivated‘)
}
vnode.componentInstance.$destroy()
}
}
watch
是在我們改變props
傳入的值時,同時對this.cache
緩存中的數據進行處理。
function pruneCache (cache: VNodeCache, filter: Function) {
for (const key in cache) {
const cachedNode: ?VNode = cache[key]
if (cachedNode) {
const name: ?string = getComponentName(cachedNode.componentOptions)
if (name && !filter(name)) {
pruneCacheEntry(cachedNode)
cache[key] = null
}
}
}
}
抽象組件沒有實際的DOM元素,所以也就沒有template
模板,它會有一個render
函數,我們就來看看裏面進行了哪些操作。
render () {
const vnode: VNode = getFirstComponentChild(this.$slots.default)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
if (name && (
(this.include && !matches(this.include, name)) ||
(this.exclude && matches(this.exclude, name))
)) {
return vnode
}
const key: ?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.key
if (this.cache[key]) {
vnode.componentInstance = this.cache[key].componentInstance
} else {
this.cache[key] = vnode
}
vnode.data.keepAlive = true
}
return vnode
}
首先,調用getFirstComponentChild
方法,來獲取this.$slots.default
中的第一個元素。
export function getFirstComponentChild (children: ?Array<VNode>): ?VNode {
return children && children.filter((c: VNode) => c && c.componentOptions)[0]
}
this.$slots.default
中包含的是什麽內容,我們在《slot和作用域插槽》中已經詳細的做了講解。
從上面的方法我們可以看到,在我們會過濾掉非自定義的標簽,然後獲取第一個自定義標簽所對應的vnode
。
所以,如果keep-alive
裏面包裹的是html
標簽,是不會渲染的。
然後獲取componentOptions
,
vdom——VNode中我們介紹過componentOptions
包含五個元素
{ Ctor, propsData, listeners, tag, children }
。
function getComponentName (opts: ?VNodeComponentOptions): ?string {
return opts && (opts.Ctor.options.name || opts.tag)
}
通過getComponentName
方法來獲取組件名,然後判斷該組件是否合法,
如果include
不匹配或exclude
匹配,則說明該組件不需要緩存,
此時直接返回該vnode
。
否則,vnode.key
不存在則生成一個,存在則就用vnode.key
作為key
。
然後把該vnode
添加到this.cache
中,並設置vnode.data.keepAlive = true
。
最終返回該vnode
。
以上只是render
函數執行的過程,keep-alive
本身也是一個組件,
在render
函數調用生成vnode
後,同樣會走__patch__
。在創建和diff
的過程中,
也會調用init
、prepatch
、insert
和destroy
鉤子函數。
不過,每個鉤子函數中所做的處理,和普通組件有所不同。
init (
vnode: VNodeWithData,
hydrating: boolean,
parentElm: ?Node,
refElm: ?Node
): ?boolean {
if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance,
parentElm,
refElm
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
} else if (vnode.data.keepAlive) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
}
},
在keep-alive
組件內調用__patch__
時,如果render
返回的vnode
是第一次使用,
則走正常的創建流程,如果之前創建過且添加了vnode.data.keepAlive
,
則直接調用prepatch
方法,且傳入的新舊vnode
相同。
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},
prepatch
函數做了哪些工作,之前也詳細的介紹過,這裏就不多說了。
簡單的總結,就是依據新vnode
中的數據,更新組件內容。
insert (vnode: MountedComponentVNode) {
if (!vnode.componentInstance._isMounted) {
vnode.componentInstance._isMounted = true
callHook(vnode.componentInstance, ‘mounted‘)
}
if (vnode.data.keepAlive) {
activateChildComponent(vnode.componentInstance, true /* direct */)
}
},
在組件插入到頁面後,如果是vnode.data.keepAlive
則會調用activateChildComponent
,
這裏面主要是調用子組件的activated
鉤子函數,並設置vm._inactive
的標識狀態。
destroy (vnode: MountedComponentVNode) {
if (!vnode.componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
vnode.componentInstance.$destroy()
} else {
deactivateChildComponent(vnode.componentInstance, true /* direct */)
}
}
}
在組件銷毀時,如果是vnode.data.keepAlive
返回true
,
則只調用deactivateChildComponent
,這裏面主要是調用子組件的deactivated
鉤子函數,
並設置vm._directInactive
的標識狀態。因為vnode.data.keepAlive
為true
的組件,
是會被keep-alive
緩存起來的,所以不會直接調用它的$destroy()
方法,
上面我們也提到了,當keep-alive
組件被銷毀時,會觸發它緩存中所有組件的$destroy()
。
因為keep-alive
包裹的組件狀態變化,還會觸發其子組件的activated
或deactivated
鉤子函數,
activateChildComponent
和deactivateChildComponent
也會做一些這方面的處理,細節大家可以自行查看。
組件的 keep-alive 簡介