keep-alive的設(shè)計(jì)初衷
有些業(yè)務(wù)場景需要根據(jù)不同的判斷條件,動(dòng)態(tài)地在多個(gè)組件之間切換。頻繁的組件切換會(huì)導(dǎo)致組件反復(fù)渲染,如果組件包含有大量的邏輯和dom節(jié)點(diǎn),極易造成性能問題。其次,切換后組件的狀態(tài)也會(huì)完全丟失。keep-alive的設(shè)計(jì)初衷就是為了保持組件的狀態(tài),避免組件的重復(fù)渲染。
為什么keep-alive可以直接使用
開發(fā)者無需注冊和引入,直接可以在模板中使用。 跟開發(fā)者使用Vue.component自定義的組件不同,keep-alive無需注冊,在模板中直接可以使用,如下所示:
<keep-alive> <component :is="view"></component> </keep-alive>
這是因?yàn)閗eep-alive是vue的內(nèi)置組件,已經(jīng)在vue中提前定義。
// 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 () { // keep-alive的銷毀,將所有緩存的組件清除 for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys) } }, mounted () { // 如果指定了include和exclude屬性,需要實(shí)時(shí)觀察當(dāng)前這兩個(gè)屬性的變化,以及時(shí)的更新緩存 this.$watch("include", val => { pruneCache(this, name => matches(val, name)) }) this.$watch("exclude", val => { pruneCache(this, name => !matches(val, name)) }) }, render () { // keepAlive組件本身不會(huì)被渲染成dom節(jié)點(diǎn),其render方法的處理邏輯的是將其包裹的組件的vnode返回 const slot = this.$slots.default // 獲取第一個(gè)組件子節(jié)點(diǎn) 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 // 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 // 1、如果緩存中存在該vnode,從緩存中取得該組件的實(shí)例(一個(gè)組件對(duì)應(yīng)一顆vnode樹,同時(shí)一個(gè)組件對(duì)應(yīng)一個(gè)vue子類的實(shí)例),不再重新創(chuàng)建 if (cache[key]) { vnode.componentInstance = cache[key].componentInstance // make current key freshest // 將當(dāng)前的組件的key作為最新的緩存(更新其在keys數(shù)組中的順序) remove(keys, key) keys.push(key) } else { // 2、如果未命中緩存,添加到緩存 cache[key] = vnode keys.push(key) // 如果緩存超過限制,淘汰最舊的緩存 if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } } // 標(biāo)記為keepAlive組件 vnode.data.keepAlive = true } return vnode || (slot && slot[0]) } }
這是因?yàn)閗eep-alive是vue的內(nèi)置組件,已經(jīng)在vue中提前定義。
// 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 () { // keep-alive的銷毀,將所有緩存的組件清除 for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys) } }, mounted () { // 如果指定了include和exclude屬性,需要實(shí)時(shí)觀察當(dāng)前這兩個(gè)屬性的變化,以及時(shí)的更新緩存 this.$watch("include", val => { pruneCache(this, name => matches(val, name)) }) this.$watch("exclude", val => { pruneCache(this, name => !matches(val, name)) }) }, render () { // keepAlive組件本身不會(huì)被渲染成dom節(jié)點(diǎn),其render方法的處理邏輯的是將其包裹的組件的vnode返回 const slot = this.$slots.default // 獲取第一個(gè)組件子節(jié)點(diǎn) 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 // 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 // 1、如果緩存中存在該vnode,從緩存中取得該組件的實(shí)例(一個(gè)組件對(duì)應(yīng)一顆vnode樹,同時(shí)一個(gè)組件對(duì)應(yīng)一個(gè)vue子類的實(shí)例),不再重新創(chuàng)建 if (cache[key]) { vnode.componentInstance = cache[key].componentInstance // make current key freshest // 將當(dāng)前的組件的key作為最新的緩存(更新其在keys數(shù)組中的順序) remove(keys, key) keys.push(key) } else { // 2、如果未命中緩存,添加到緩存 cache[key] = vnode keys.push(key) // 如果緩存超過限制,淘汰最舊的緩存 if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } } // 標(biāo)記為keepAlive組件 vnode.data.keepAlive = true } return vnode || (slot && slot[0]) } }
// core/components/index.js import KeepAlive from "./keep-alive" export default { KeepAlive }
// core/global-api/index.js // 導(dǎo)入內(nèi)置組件 import builtInComponents from "../components/index" /** * 為Vue添加全局方法和屬性 * @param {GlobalAPI} Vue */ export function initGlobalAPI (Vue: GlobalAPI) { // ...省略了無關(guān)代碼 Vue.options = Object.create(null) // 添加內(nèi)置組件keep-alive extend(Vue.options.components, builtInComponents) }
buildInComponents中包含了keep-alive的定義。在initGlobalAPI方法中,將內(nèi)置組件添加到了 vue的全局變量中。
extend(A, B)是個(gè)簡單的對(duì)象屬性復(fù)制方法。將對(duì)象B中的屬性復(fù)制到對(duì)象A中。
keep-alive是如何保持組件狀態(tài)的
為了保持組件狀態(tài),keep-alive設(shè)計(jì)了緩存機(jī)制。
我們知道,模板中的每個(gè)HTML標(biāo)簽在vue中由相應(yīng)的vnode節(jié)點(diǎn)對(duì)象來表示。如果是HTML標(biāo)簽是組件標(biāo)簽,需要為該標(biāo)簽的vnode創(chuàng)建一個(gè)組件實(shí)例。組件實(shí)例負(fù)責(zé)組件內(nèi)的HTML模板的編譯和渲染。因此相比于普通HTML標(biāo)簽的vnode節(jié)點(diǎn),組件vnode節(jié)點(diǎn)會(huì)存在componentOptions和 componentInstance 這兩個(gè)屬性中保存組件選項(xiàng)對(duì)象和組件實(shí)例的引用。
首先,我們從keep-alive組件的實(shí)現(xiàn)代碼中可以看到在組件的created鉤子中設(shè)計(jì)了緩存機(jī)制:
created () { this.cache = Object.create(null) this.keys = [] }
keep-alive設(shè)置了cache和keys兩個(gè)屬性來緩存子組件。其中cache中的每項(xiàng)是一個(gè)以所包裹的組件的組件名為key,包裹組件對(duì)應(yīng)的vnoded為值的對(duì)象。keys的每一項(xiàng)是其所包裹的組件的組件名。
render 函數(shù)是vue實(shí)例和vue組件實(shí)例中用來創(chuàng)建vnode的方法。我們在實(shí)際的應(yīng)用中,一般是通過template和el來指定模板,然后由vue將模板編譯成render函數(shù)。如果用戶希望能更靈活地控制vnode的創(chuàng)建可以提供自定義的render函數(shù)。
render () { const slot = this.$slots.default // 獲取第一個(gè)組件子節(jié)點(diǎn) 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 // 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 // 1、如果緩存中存在該vnode,從緩存中取得該組件的實(shí)例(一個(gè)組件對(duì)應(yīng)一顆vnode樹,同時(shí)一個(gè)組件對(duì)應(yīng)一個(gè)vue子類的實(shí)例),不再重新創(chuàng)建 if (cache[key]) { vnode.componentInstance = cache[key].componentInstance // make current key freshest // 將當(dāng)前的組件的key作為最新的緩存(更新其在keys數(shù)組中的順序) remove(keys, key) keys.push(key) } else { // 2、如果未命中緩存,添加到緩存 cache[key] = vnode keys.push(key) // 如果緩存超過限制,淘汰最舊的緩存 if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } } // 標(biāo)記為keepAlive組件 vnode.data.keepAlive = true } return vnode || (slot && slot[0]) }
keep-alive內(nèi)部就是單獨(dú)提供了render函數(shù)來自定義了vnode的創(chuàng)建邏輯。首先keep-alive獲取到其所包裹的子組件的根vnode,然后去cache中查找該組件是否存在。
如果cache中不存在子組件vnode,則以{子組件名: 子組件vnode}的形式保存到cache對(duì)象中。同時(shí)將子組件名字保存到keys數(shù)組中。同時(shí)如果當(dāng)前緩存的數(shù)量已經(jīng)超過max所設(shè)置的最大值,需要淘汰掉最近最少使用的緩存項(xiàng)(LRU)。
如果cache中存在子組件vnode,那么只需要復(fù)用緩存的組件vnode的組件實(shí)例(componentInstance)。同時(shí)需要將該子組件vnode在緩存中順序調(diào)到最前面,這個(gè)主要是為了在緩存不足時(shí),正確地淘汰緩存項(xiàng)。
舉例說明
最后通過一個(gè)例子加深一下理解。
<div id="app"> <keep-alive><component :is="view"></component></keep-alive> <button @click="view = view =="count"? "any": "count"">切換組件</button> </div>
Vue.component("count", { data() { return { count:0 }; }, template: "<div @click="count+=1">點(diǎn)了我 {{count}} 次</div>" }); Vue.component("any", { template: "<div>any</div>" }); new Vue({ el: "#app", data: { view: "count" } });
由于view默認(rèn)值是count,因此keep-alive包裹的子組件是count。此時(shí)keep-alive的緩存中為空,因此會(huì)把組件count的vnode添加到緩存。緩存結(jié)果為
cache = {1::count: {tag: "vue-component-1-count", data:{tag: "component", hook: {…}}}, componentOptions, componentInstance, ...} keys = ["1::count"]
點(diǎn)擊一下組件count,組件的顯示內(nèi)容變成"點(diǎn)了我1次",然后切換到組件any。與count組件相同,由于在keep-alive的緩存中還未保存any組件的vnode,因此需要將any添加到緩存中。此時(shí)緩存結(jié)果變成了:
cache = { 1::count: {tag: "vue-component-1-count", data:{tag: "component", hook: {…}}, componentOptions, componentInstance, ...}, 2::any: {tag: "vue-component-2-any", data:{tag: "component", hook: {…}}, componentOptions, componentInstance, ...}, } keys = ["1::count", "2::any"]
頁面顯示結(jié)果為:
再次點(diǎn)擊切換組件,切回count。此時(shí)count組件的vnode在緩存中已經(jīng)存在,所以直接復(fù)用了原來count組件vnode中所保存的組件實(shí)例,組件實(shí)例中保存了原來的count值,因此組件切換完后,組件的狀態(tài)也跟著還原了回來。
下圖為count組件實(shí)例的狀態(tài),可以看到count的值得到了保持:
最終頁面顯示結(jié)果為:
從上面的分析可知,如果組件被包裹在keep-alive組件中,組件vnode會(huì)緩存到cache中。而組件的狀態(tài)又會(huì)保存在組件實(shí)例中(componentInstance),當(dāng)組件再次切換回來時(shí),keep-alive直接將之前緩存的狀態(tài)進(jìn)行還原,也就實(shí)現(xiàn)了組件狀態(tài)的保持。
以上就是keep-alive保持組件狀態(tài)的方法的詳細(xì)內(nèi)容,更多關(guān)于keep-alive保持組件狀態(tài)的資料請(qǐng)關(guān)注服務(wù)器之家其它相關(guān)文章!
原文鏈接:https://juejin.cn/post/6901224360783446030?utm_source=tuicool&utm_medium=referral