使用場景:
列表查詢出約課記錄之后五垮,跳轉(zhuǎn)到詳情后再返回頁面內(nèi)容刷新扎附,之前搜索的結(jié)果就沒有了抠忘,為了解決這一問題撩炊,使用了keep-alive組件來實(shí)現(xiàn)頁面緩存。
實(shí)現(xiàn)方案:
keep-alive
實(shí)現(xiàn)原理:
- keep-alive是vue2.0提供的用來緩存的內(nèi)置組件崎脉,避免多次加載相同的組建拧咳,減少性能消耗。(keep-alive.js)
- 將需要緩存的VNode節(jié)點(diǎn)保存在this.cache中(而不是直接存儲(chǔ)DOM結(jié)構(gòu))囚灼,在render時(shí)骆膝,如果VNode的name符合緩存條件,則直接從this.cache中取出緩存的VNode實(shí)例進(jìn)行渲染灶体。
- keep-alive的渲染是在patch階段阅签,在已經(jīng)緩存的情況下不會(huì)進(jìn)入$mount階段,所以mounted之前的鉤子只會(huì)執(zhí)行一次蝎抽。
keep-alive.js源碼:
// src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true, // 判斷當(dāng)前組件虛擬dom是否渲染成真實(shí)dom的關(guān)鍵
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() {
// 實(shí)時(shí)監(jiān)聽黑白名單的變動(dòng)
this.$watch('include', val => {
pruneCache(this, name => matched(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render() {
// 先省略...
}
}
render函數(shù)解讀
- 通過getFirstComponentChild獲取第一個(gè)組件(vnode)愉择;
- 獲取該組件的name(有name的返回name,無name返回標(biāo)簽名)织中;
- 將這個(gè)name通過include、exclude屬性進(jìn)行匹配衷戈;
- 匹配不成功說明不需要緩存狭吼,直接返回vnode;
- 匹配成功后殖妇,根據(jù)key在this.cache中查找是否已經(jīng)被緩存過刁笙;
- 如果已緩存過,將緩存的VNode的組件實(shí)例componentInsance覆蓋到當(dāng)前vnode上谦趣,并返回疲吸;
- 未被緩存則將VNode存儲(chǔ)在this.cache中;
render () {
/* 得到slot插槽中的第一個(gè)組件 */
const vnode: VNode = getFirstComponentChild(this.$slots.default)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
/* 獲取組件名稱前鹅,優(yōu)先獲取組件的name字段摘悴,否則是組件的tag */
const name: ?string = getComponentName(componentOptions)
/* name不在inlcude中或者在exlude中則直接返回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
/* 如果已經(jīng)做過緩存了則直接從緩存中獲取組件實(shí)例給vnode,還未緩存過則進(jìn)行緩存 */
if (this.cache[key]) {
vnode.componentInstance = this.cache[key].componentInstance
} else {
this.cache[key] = vnode
}
/* keepAlive標(biāo)記位 */
vnode.data.keepAlive = true
}
return vnode
}
渲染階段
只執(zhí)行一次的鉤子:當(dāng)vnode.componentInstance和keepAlive為true時(shí)舰绘,不再進(jìn)入$mount過程蹂喻,也就不會(huì)執(zhí)行mounted以及之前的鉤子函數(shù)(beforeCreated、created捂寿、mounted)
// 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)
}
}
// ...
}
可重復(fù)執(zhí)行的activated:在patch階段口四,最后會(huì)執(zhí)行invokeInsertHook函數(shù),這個(gè)函數(shù)調(diào)用組件實(shí)例的insert鉤子秦陋,insert鉤子中調(diào)用了activateChildComponent函數(shù)蔓彩,遞歸執(zhí)行子組件中的activated鉤子函數(shù)
// 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]) // 調(diào)用VNode自身的insert鉤子函數(shù)
}
}
}
// 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 */)
}
}
// ...
}
頁面緩存的使用方法:
- keep-alive組件提供了兩個(gè)屬性include、exclude,可以用逗號隔開的字符串或正則表達(dá)式赤嚼、一個(gè)數(shù)組來表示旷赖。
- keep-alive的生命周期函數(shù)created、mounted只有創(chuàng)建時(shí)會(huì)被觸發(fā)一次
- 生命周期鉤子有兩個(gè)activated探膊、deactivated杠愧,分別在組件激活、非激活狀態(tài)時(shí)觸發(fā)
- 因?yàn)閗eep-alive將組件緩存起來逞壁,不會(huì)被銷毀和重建流济,所以不會(huì)重新調(diào)用created、mounted方法腌闯。
- 需要重置data數(shù)據(jù)Object.assign(this.
options.data.call(this))
- 從指定路由跳轉(zhuǎn)回來需要刷新的情況绳瘟,可以結(jié)合路由守衛(wèi)beforeRouterEnter和beforeRouterLeave來區(qū)分是否需要刷新
- 使用vue-devtool觀察組件的緩存狀態(tài),灰色的為被緩存起來的狀態(tài)
pageList → pageDetail → pageList姿骏,pageList保存原有狀態(tài)糖声;pageList → 其他 → pageList,pageList初始化狀態(tài)
使用方法一
// 使用router的meta屬性控制
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta || !$route.meta.keepAlive" class="main"></router-view>
// router.js
{
path: 'pageList',
name: 'pageList',
component: () => import('../pages/pageList.vue'),
meta: {
// keepAlive是否使用keep-alive組件分瘦,isUseCache是否需要緩存
keepAlive: true, isUseCache: false
}
}
// pageList.vue
<template>
<div></div>
</template>
<script>
export default {
name: 'pageList',
data() {
return {}
},
activated() {
// 組件活動(dòng)狀態(tài)
if (!this.$route.meta.isUseCache) {
// 不需要緩存時(shí)需要重置數(shù)據(jù)
Object.assign(this.$data, this.$options.data.call(this))
// 設(shè)置為需要緩存
this.$route.meta.isUseCache = true
}
},
deactivated() {
// 組件非活躍狀態(tài)
},
beforeRouteEnter(to, from, next) {
// 從pageA頁面跳轉(zhuǎn)到pageB需要緩存蘸泻,其他頁面跳轉(zhuǎn)會(huì)pageB不需要緩存
if(from.name != 'pageDetail'){
to.meta.isUseCache = false;
} else {
to.meta.isUseCache = true
}
},
}
</script>
pageList → pageDetail → pageList,pageList保存原有狀態(tài)嘲玫;pageList → 其他 → pageList悦施,pageList初始化狀態(tài)
使用方法二:
// 使用keep-alive的prop屬性
// cacheList可以存儲(chǔ)在vuex中,默認(rèn)為'pageList'
<keep-alive :include="cacheList">
<router-view></router-view>
</keep-alive>
// pageList.vue
beforeRouteLeave (to, from, next) {
if (to.name !== 'pageDetail') {
this.$store.dispatch('setCacheList', '')
}else {
this.$store.dispatch('setCacheList', 'pageList')
}
next()
}
問題:方法二中,pageList → pageDetail → 其他 → pageList去团,pageList采用了緩存的數(shù)據(jù)抡诞, 解決:其他頁面中的beforeRouteLeave增加 this.$store.dispatch('setCacheList', '')