keep-alive是Vue.js的一個內置組件。它能夠不活動的組件實例保存在內存中,我們來探究一下它的源碼實現。
首先回顧下使用方法
舉個栗子
<keep-alive>
<component-a v-if="isShow"></component-a>
<component-b v-else></component-b>
</keep-alive>
<button @click="test=handleClick">請點擊</button>
export default {
data () {
return {
isShow: true
}
},
methods: {
handleClick () {
this.isShow = !this.isShow;
}
}
}
在點擊按鈕時,兩個組件會發生切換,但是這時候這兩個組件的狀態會被緩存起來,比如:組件中都有一個input標籤,那麼input標籤中的內容不會因爲組件的切換而消失。
屬性支持
keep-alive組件提供了include
與exclude
兩個屬性來允許組件有條件地進行緩存,二者都可以用逗號分隔字符串、正則表達式或一個數組來表示。
舉個例子:
- 緩存
name
爲a的組件。
<keep-alive include="a">
<component></component>
</keep-alive>
- 排除緩存name爲a的組件。
<keep-alive exclude="a">
<component></component>
</keep-alive>
當然 props 還定義了 max,該配置允許我們指定緩存大小。
keep-alive 源碼實現
說完了keep-alive組件的使用,我們從源碼角度看一下keep-alive組件究竟是如何實現組件的緩存的呢?
創建和銷燬階段
首先看看 keep-alive
的創建和銷燬階段做了什麼事情:
created () {
/* 緩存對象 */
this.cache = Object.create(null)
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache[key])
}
},
- 在
keep-alive
的創建階段: created鉤子會創建一個cache
對象,用來保存vnode節點。 - 在銷燬階段:destroyed 鉤子則會調用
pruneCacheEntry
方法清除cache緩存中的所有組件實例。
pruneCacheEntry
方法的源碼實現
/* 銷燬vnode對應的組件實例(Vue實例) */
function pruneCacheEntry (vnode: ?VNode) {
if (vnode) {
vnode.componentInstance.$destroy()
}
}
因爲keep-alive會將組件保存在內存中,並不會銷燬以及重新創建,所以不會重新調用組件的created等方法,因此keep-alive提供了兩個生命鉤子,分別是activated
與deactivated
。用這兩個生命鉤子得知當前組件是否處於活動狀態。(稍後會看源碼如何實現)
渲染階段
render () {
/* 得到slot插槽中的第一個組件 */
const vnode: VNode = getFirstComponentChild(this.$slots.default)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// 獲取組件名稱,優先獲取組件的name字段,否則是組件的tag
const name: ?string = getComponentName(componentOptions)
// 不需要緩存,則返回 vnode
if (name && (
(this.include && !matches(this.include, name)) ||
(this.exclude && matches(this.exclude, name))
)) {
return vnode
}
const key: ?string = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (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)
}
}
// keepAlive標記
vnode.data.keepAlive = true
}
return vnode
}
render 做了以下事情:
- 通過getFirstComponentChild獲取第一個子組件,獲取該組件的name(存在組件名則直接使用組件名,否則會使用tag)
- 將name通過include與exclude屬性進行匹配,匹配不成功(說明不需要緩存)則直接返回vnode
- 匹配成功則嘗試獲取緩存的組件實例
- 若沒有緩存該組件,則緩存該組件
- 緩存超過最大值會刪掉第一個緩存
name 匹配的方法(校驗是逗號分隔的字符串還是正則)
/* 檢測name是否匹配 */
function matches (pattern: string | RegExp, name: string): boolean {
if (typeof pattern === 'string') {
/* 字符串情況,如a,b,c */
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
/* 正則 */
return pattern.test(name)
}
/* istanbul ignore next */
return false
}
如果在中途有對 include
和 exclude
進行修改該怎麼辦呢?
作者通過 watch
來監聽 include
和 exclude
,在其改變時調用 pruneCache
以修改 cache
緩存中的緩存數據。
watch: {
/* 監視include以及exclude,在被修改的時候對cache進行修正 */
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
做了什麼?
// 修補 cache
function pruneCache (cache: VNodeCache, current: VNode, filter: Function) {
for (const key in cache) {
// 嘗試獲取 cache中的vnode
const cachedNode: ?VNode = cache[key]
if (cachedNode) {
const name: ?string = getComponentName(cachedNode.componentOptions)
if (name && !filter(name)) { // 重新篩選組件
if (cachedNode !== current) { // 不在當前 _vnode 中
pruneCacheEntry(cachedNode) // 調用組件實例的 銷燬方法
}
cache[key] = null // 移除該緩存
}
}
}
}
pruneCache
方法 遍歷cache中的所有項,如果不符合規則則會銷燬該節點並移除該緩存
進階
再回顧下源碼,在 src/core/components/keep-alive.js
中
export default {
name: 'keep-alive,
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created () {
this.cache = Object.create(null)
this.keys = []
},
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 () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
現在不加註釋也應該大部分都能看懂了?
順便提下 abstract
這個屬性,若 abstract
爲 true
,則表示組件是一個抽象組件,不會被渲染到真實DOM中,也不會出現在父組件鏈中。
那麼爲什麼在組件有緩存的時候不會再次執行組件的 created
、mounted
等鉤子函數呢?
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
// 進入這段邏輯
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
const mountedNode: any = vnode
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = 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
函數。
function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
const hasChildren = !!(
renderChildren ||
vm.$options._renderChildren ||
parentVnode.data.scopedSlots ||
vm.$scopedSlots !== emptyObject
)
// ...
if (hasChildren) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
}
keep-alive
組件本質上是通過 slot
實現的,所以它執行 prepatch
的時候,hasChildren = true
,會觸發組件的 $forceUpdate
邏輯,也就是重新執行 keep-alive
的 render 方法
然鵝,根據上面講的 render 方法源碼,就會去找緩存咯。
那麼,<keep-alive>
的實現原理就介紹完了
最後
- 原創不易點個讚唄
- 歡迎關注公衆號「前端進階課」認真學前端,一起進階。