keep-alive是Vue.js的一個內置組件。它可以不活動的組件實例保存在內存中,咱們來探究一下它的源碼實現。html
舉個栗子前端
<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標籤中的內容不會由於組件的切換而消失。node
keep-alive組件提供了include
與exclude
兩個屬性來容許組件有條件地進行緩存,兩者均可以用逗號分隔字符串、正則表達式或一個數組來表示。正則表達式
舉個例子:數組
name
爲a的組件。<keep-alive include="a"> <component></component> </keep-alive>
<keep-alive exclude="a"> <component></component> </keep-alive>
固然 props 還定義了 max,該配置容許咱們指定緩存大小。緩存
說完了keep-alive組件的使用,咱們從源碼角度看一下keep-alive組件到底是如何實現組件的緩存的呢?函數
首先看看 keep-alive
的建立和銷燬階段作了什麼事情:this
created () { /* 緩存對象 */ this.cache = Object.create(null) }, destroyed () { for (const key in this.cache) { pruneCacheEntry(this.cache[key]) } },
keep-alive
的建立階段: created鉤子會建立一個cache
對象,用來保存vnode節點。pruneCacheEntry
方法清除cache緩存中的全部組件實例。pruneCacheEntry
方法的源碼實現spa
/* 銷燬vnode對應的組件實例(Vue實例) */ function pruneCacheEntry (vnode: ?VNode) { if (vnode) { vnode.componentInstance.$destroy() } }
由於keep-alive會將組件保存在內存中,並不會銷燬以及從新建立,因此不會從新調用組件的created等方法,所以keep-alive提供了兩個生命鉤子,分別是activated
與deactivated
。用這兩個生命鉤子得知當前組件是否處於活動狀態。(稍後會看源碼如何實現)code
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 作了如下事情:
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>
的實現原理就介紹完了