原文連接:github.com/qi...javascript
本文介紹的內容包括:html
keep-alive是一個抽象組件:它自身不會渲染一個 DOM 元素,也不會出如今父組件鏈中;使用keep-alive包裹動態組件時,會緩存不活動的組件實例,而不是銷燬它們。vue
用戶在某個列表頁面選擇篩選條件過濾出一份數據列表,由列表頁面進入數據詳情頁面,再返回該列表頁面,咱們但願:列表頁面能夠保留用戶的篩選(或選中)狀態。keep-alive就是用來解決這種場景。固然keep-alive不只僅是可以保存頁面/組件的狀態這麼簡單,它還能夠避免組件反覆建立和渲染,有效提高系統性能。 總的來講,keep-alive用於保存組件的渲染狀態。java
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
<component :is="currentComponent"></component>
</keep-alive>
複製代碼
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
<router-view></router-view>
</keep-alive>
複製代碼
include
定義緩存白名單,keep-alive會緩存命中的組件;exclude
定義緩存黑名單,被命中的組件將不會被緩存;max
定義緩存組件上限,超出上限使用LRU的策略置換緩存數據。node
keep-alive.js
內部另外還定義了一些工具函數,咱們按住不表,先看它對外暴露的對象。react
// src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true, // 判斷當前組件虛擬dom是否渲染成真是dom的關鍵
props: {
include: patternTypes, // 緩存白名單
exclude: patternTypes, // 緩存黑名單
max: [String, Number] // 緩存的組件實例數量上限
},
created () {
this.cache = Object.create(null) // 緩存虛擬dom
this.keys = [] // 緩存的虛擬dom的健集合
},
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 () {
// 先省略...
}
}
複製代碼
能夠看出,與咱們定義組件的過程同樣,先是設置組件名爲keep-alive
,其次定義了一個abstract
屬性,值爲true
。這個屬性在vue的官方教程並未說起,卻相當重要,後面的渲染過程會用到。props
屬性定義了keep-alive組件支持的所有參數。git
keep-alive在它生命週期內定義了三個鉤子函數:github
createdvue-router
初始化兩個對象分別緩存VNode(虛擬DOM)和VNode對應的鍵集合緩存
destroyed
刪除this.cache
中緩存的VNode實例。咱們留意到,這裏不是簡單地將this.cache
置爲null
,而是遍歷調用pruneCacheEntry
函數刪除。
// src/core/components/keep-alive.js
function pruneCacheEntry ( cache: VNodeCache, key: string, keys: Array<string>, current?: VNode ) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy() // 執行組件的destory鉤子函數
}
cache[key] = null
remove(keys, key)
}
複製代碼
刪除緩存VNode還要對應執行組件實例的destory
鉤子函數。
mounted
在mounted
這個鉤子中對include
和exclude
參數進行監聽,而後實時地更新(刪除)this.cache
對象數據。pruneCache
函數的核心也是去調用pruneCacheEntry
。
render
// src/core/components/keep-alive.js
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot) // 找到第一個子組件對象
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) { // 存在組件參數
// check pattern
const name: ?string = getComponentName(componentOptions) // 組件名
const { include, exclude } = this
if ( // 條件匹配
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null // 定義組件的緩存key
// 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 (cache[key]) { // 已經緩存過該組件
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key) // 調整key排序
} else {
cache[key] = vnode // 緩存組件對象
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) { // 超過緩存數限制,將第一個刪除
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true // 渲染和執行被包裹組件的鉤子函數須要用到
}
return vnode || (slot && slot[0])
}
複製代碼
key
在this.keys
中的位置(更新key的位置是實現LRU置換策略的關鍵),不然執行第四步;this.cache
對象中存儲該組件實例並保存key
值,以後檢查緩存的實例數量是否超過max
的設置值,超過則根據LRU置換策略刪除最近最久未使用的實例(便是下標爲0的那個key)。keepAlive
屬性值設置爲true
。這個在@不可忽視:鉤子函數 章節會再次出場。借一張圖看下Vue渲染的整個過程:
Vue的渲染是從圖中的render
階段開始的,但
keep-alive的渲染是在patch階段,這是構建組件樹(虛擬DOM樹),並將VNode轉換成真正DOM節點的過程。
簡單描述從render
到patch
的過程
咱們從最簡單的new Vue
開始:
import App from './App.vue'
new Vue({
render: h => h(App),
}).$mount('#app')
複製代碼
_render
函數將組件對象轉化爲一個VNode實例;而_render
是經過調用createElement
和createEmptyVNode
兩個函數進行轉化;createElement
的轉化過程會根據不一樣的情形選擇new VNode
或者調用createComponent
函數作VNode實例化;_update
函數把VNode渲染爲真實DOM,這個過程又是經過調用__patch__
函數完成的(這就是pacth階段了)用一張圖表達:
咱們用過keep-alive都知道,它不會生成真正的DOM節點,這是怎麼作到的?
// src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
const options = vm.$options
// 找到第一個非abstract的父組件實例
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
// ...
}
複製代碼
Vue在初始化生命週期的時候,爲組件實例創建父子關係會根據abstract
屬性決定是否忽略某個組件。在keep-alive中,設置了abstract: true
,那Vue就會跳過該組件實例。
最後構建的組件樹中就不會包含keep-alive組件,那麼由組件樹渲染成的DOM樹天然也不會有keep-alive相關的節點了。
keep-alive包裹的組件是如何使用緩存的?
在patch
階段,會執行createComponent
函數:
// src/core/vdom/patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm) // 將緩存的DOM(vnode.elm)插入父元素中
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
複製代碼
keep-alive.js
中的render
函數可知,vnode.componentInstance
的值是undefined
,keepAlive
的值是true
,由於keep-alive組件做爲父組件,它的render
函數會先於被包裹組件執行;那麼就只執行到i(vnode, false /* hydrating */)
,後面的邏輯再也不執行;vnode.componentInstance
的值就是已經緩存的組件實例,那麼會執行insert(parentElm, vnode.elm, refElm)
邏輯,這樣就直接把上一次的DOM插入到了父元素中。通常的組件,每一次加載都會有完整的生命週期,即生命週期裏面對應的鉤子函數都會被觸發,爲何被keep-alive包裹的組件卻不是呢? 咱們在@源碼剖析 章節分析到,被緩存的組件實例會爲其設置keepAlive = true
,而在初始化組件鉤子函數中:
// src/core/vdom/create-component.js
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
// ...
}
複製代碼
能夠看出,當vnode.componentInstance
和keepAlive
同時爲truly值時,再也不進入$mount
過程,那mounted
以前的全部鉤子函數(beforeCreate
、created
、mounted
)都再也不執行。
在patch
的階段,最後會執行invokeInsertHook
函數,而這個函數就是去調用組件實例(VNode)自身的insert
鉤子:
// src/core/vdom/patch.js
function invokeInsertHook (vnode, queue, initial) {
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i]) // 調用VNode自身的insert鉤子函數
}
}
}
複製代碼
再看insert
鉤子:
// src/core/vdom/create-component.js
const componentVNodeHooks = {
// init()
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
// ...
}
複製代碼
在這個鉤子裏面,調用了activateChildComponent
函數遞歸地去執行全部子組件的activated
鉤子函數:
// src/core/instance/lifecycle.js
export function activateChildComponent (vm: Component, direct?: boolean) {
if (direct) {
vm._directInactive = false
if (isInInactiveTree(vm)) {
return
}
} else if (vm._directInactive) {
return
}
if (vm._inactive || vm._inactive === null) {
vm._inactive = false
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i])
}
callHook(vm, 'activated')
}
}
複製代碼
相反地,deactivated
鉤子函數也是同樣的原理,在組件實例(VNode)的destroy
鉤子函數中調用deactivateChildComponent
函數。