# 2-3 動態元件管理
前面我們曾介紹 Vue.js 拆分元件的方式,也提到拆分後的元件可以透過 v-if
、 v-show
等指令搭配條件式來處理元件的使用 (或顯示) 與否。
在這個小節,我們將延續這個題目,繼續探討在 Vue.js 動態處理元件會遇到哪些問題以及對應的解決方案。
# v-bind:is
與動態元件
在過去,若我們想要動態控制元件的出現與否,通常會使用 v-if
來處理:
<div id="app">
<button
v-for="tab in tabs"
:key="tab"
:class="['tab-button', { active: currentTab === tab }]"
@click="currentTab = tab">
{{ tab }}
</button>
<!-- 用 v-if 來判斷元件的顯示或隱藏 -->
<tab-home v-if="currentTab === 'Home'"></tab-home>
<tab-posts v-if="currentTab === 'Posts'"></tab-posts>
<tab-archive v-if="currentTab === 'Archive'"></tab-archive>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
const app = Vue.createApp({
data() {
return {
currentTab: 'Home',
tabs: ['Home', 'Posts', 'Archive']
}
}
});
// 分別註冊三個子元件
app.component('tab-home', {
template: `<div class="demo-tab">Home component</div>`
});
app.component('tab-posts', {
template: `<div class="demo-tab">Post component</div>`
});
app.component('tab-archive', {
template: `<div class="demo-tab">Archive component</div>`
});
app.mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
這樣的做法雖然可以做到動態處理元件的渲染與否,不過 Vue.js 同時也提供了另一個特殊的屬性 is
來幫助我們更簡潔地做到這件事。
先在上層元件新增 currentTabComponent
這個 computed
屬性:
const app = Vue.createApp({
data() {
return {
currentTab: 'Home',
tabs: ['Home', 'Posts', 'Archive']
}
},
computed: {
currentTabComponent () {
return `tab-${ this.currentTab.toLowerCase() }`;
}
}
});
2
3
4
5
6
7
8
9
10
11
12
13
然後用一個 <component>
來取代 <tab-home>
、 <tab-posts>
與 <tab-archive>
<component :is="currentTabComponent"></component>
像這樣,我們只需要透過 <component>
標籤,加上 v-bind:is
(也可簡寫為 :is
) ,即可完成動態切換元件的功能。
# <keep-alive>
保留元件狀態
在完成了快速切換元件的功能後,我們來改寫前面的範例:
// 註:此三個元件的 data 未用到 this,所以直接使用箭頭函式回傳物件
app.component('tab-home', {
template: `<div class="demo-tab"><input v-model="title"></div>`,
data: () => ({ title: 'Home component' })
});
app.component('tab-posts', {
template: `<div class="demo-tab"><input v-model="title"></div>`,
data: () => ({ title: 'Post component' })
});
app.component('tab-archive', {
template: `<div class="demo-tab"><input v-model="title"></div>`,
data: () => ({ title: 'Archive component' })
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
像這樣,我們為前面三個子元件分別加入了 <input>
輸入框,並使用 v-model
指令與 title
屬性綁定。
此時試著修改輸入框內文字,再反覆切換上方的頁籤後,應該會發現到,當頁籤被切換後,原本更新的內容並不會被留存。
過去,遇到這類問題時我們會將 v-if
改為 v-show
避開元件銷毀與重建來處理這個問題,
但現在我們已經改用 :is
來動態切換元件,那麼還有什麼方法可以保留切換後的元件狀態呢?
針對這個問題 Vue.js 也提供了 <keep-alive>
這個特殊的「元件」標籤來暫存保留元件當下的狀態。
使用方式很簡單,只要在需要保留的元件外用 <keep-alive>
標籤包住即可:
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
2
3
另外, <keep-alive>
除了可以搭配 :is
來使用外,也可以在 v-if
指令上搭配使用:
<keep-alive>
<component-a v-if="a > 1"> ... </component-a>
<component-b v-else> ... </component-b>
</keep-alive>
2
3
4
不過要注意的是,由於 <keep-alive>
同時間只會有一個子元件會被渲染,且也要避免 v-if
與 v-for
共同使用造成的問題。
# include
、 exclude
與 max
屬性
要是切換的子元件數量太多,每個都加上 keep-alive
必定會造成某種程度上的效能成本。
若我們只想針對某部分的子元件進行暫存快取 (或排除某些子元件不要 keep-alive
),
則可以透過 <keep-alive>
提供的 include
與 exclude
屬性來處理,如:
<!-- 只保留 tab-home 與 tab-posts 的狀態 -->
<keep-alive include="tab-home,tab-posts">
<component :is="currentTabComponent"></component>
</keep-alive>
2
3
4
需要注意的是,若使用 include
與 exclude
屬性時,子元件需要加上 name
屬性來提供識別:
app.component('tab-home', {
name: 'tab-home',
template: `<div class="demo-tab"><input v-model="title"></div>`,
data: () => ({ title: 'Home component' })
});
app.component('tab-posts', {
name: 'tab-posts',
template: `<div class="demo-tab"><input v-model="title"></div>`,
data: () => ({ title: 'Post component' })
});
app.component('tab-archive', {
name: 'tab-archive',
template: `<div class="demo-tab"><input v-model="title"></div>`,
data: () => ({ title: 'Archive component' })
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
除了逗點分隔外,也可以利用 Regular Expression 或陣列的形式:
<!-- 以 RegExp 作為 include 的條件, 記得加上 v-bind: -->
<keep-alive :include="/tab-(home|posts)/">
<component v-bind:is="currentTabComponent" />
</keep-alive>
<!-- 或以陣列做為 include 條件 -->
<keep-alive :include="['tab-home', 'tab-posts']">
<component v-bind:is="currentTabComponent" />
</keep-alive>
2
3
4
5
6
7
8
9
當然 exclude
屬性的用法也是一樣,差別只在一個是列入,另一個是排除。
小提醒
include
與 exclude
對應的條件為子元件的 name
屬性,而不是子元件的標籤名。
像是:
app.component('my-component', {
name: 'Home-Component'
});
2
3
那麼在使用 include/exclude
的時候就必須寫成 :include="'Home-Component'"
而不是 'my-component'
。
另外,為了避免元件數量過多造成的性能浪費, <keep-alive>
也提供了 max
屬性來提供開發者指定瀏覽器暫存的元件數量:
<keep-alive :max="2">
<component v-bind:is="currentTabComponent" />
</keep-alive>
2
3
此時,我們就可以透過 :max
來指定暫存的子元件數量, <keep-alive>
只會保留最後引入的兩個子元件狀態。
而 :max
也可與前面的 :include
或 :exclude
搭配使用,提供了開發上的靈活性。
被 <keep-alive>
標籤包覆後的元件,即使在切換前修改了資料,再次切換回來時仍能保有原來的狀態。
像此類元件其實就如同我們在 2-1 小節 元件系統的特性 所提到的 「功能型元件」那樣的特殊功能來使用。
# 特殊的生命週期 Hooks: activated
與 deactivated
與其他元件不同的是, Vue.js 擴充了兩組 Hooks function 給 <keep-alive>
來使用,分別是 activated
與 deactivated
這兩個 lifecycle hook。
在介紹這兩個 hook 前,讓我們改寫一下前面的例子,
分別在 created
、 mounted
以及 unmounted
階段加入觀察訊息,並且把 <keep-alive>
標籤移除:
// 透過 this.$options.name 可以取得子元件的 name 屬性
created () {
this.$emit('update', `${this.$options.name} Created.`);
},
mounted () {
this.$emit('update', `${this.$options.name} Mounted.`);
},
unmounted () {
this.$emit('update', `${this.$options.name} Unmounted.`);
}
2
3
4
5
6
7
8
9
10
當元件在動態切換的時候,若是沒有加上 <keep-alive>
標籤,則會在切換元件時重新觸發元件的各種生命週期階段。
元件建立時,如同我們在前面介紹過的,會先從 created
開始,然後是 mounted
階段。
而切換元件時的執行順序是 「建立新的元件」(created
) → 「銷毀目前元件」(unmounted
) → 「掛載新的元件」(mounted
)。
現在,讓我們再加回 <keep-alive>
標籤,並且加上 activated
與 deactivated
這兩個 hook (原本的 unmounted
未移除) :
activated () {
this.$emit('update', `${this.$options.name} Activated.`);
},
deactivated () {
this.$emit('update', `${this.$options.name} Deactivated.`);
},
2
3
4
5
6
此時若在加入 <keep-alive>
的情況下,在元件首次建立時,就會先進行 created
→ mounted
→ activated
三個階段,
而當我們切換至新的元件時,則會依序執行:
「建立新的元件」(created
) → 「暫停目前元件」(deactivated
) → 「掛載新的元件」(mounted
) → 「啟用新的元件」(activated
) 這幾個階段。
倘若前面已經執行過 created
階段而未被銷毀的元件,當它再次被啟用 (activated
) 的時候,也只會執行 activated
hook 而不是從 created
階段重新建立它的生命週期了。
小提醒
有關元件生命週期的部分,讀者如果不熟悉可回頭參閱本書 1-7 元件的生命週期與更新機制 小節。
# 非同步元件 Async Component (Vue 3.x 新增)
在中、大規模的網頁應用裡,為了執行上的效能考量,我們有時候需要將元件拆分成多個檔案來進行管理維護,直到需要的時候才將它載入至網頁裡,
過去,我們可以透過 import
某個元件檔案來達到非同步載入元件的效果,如:
const asyncPage = {
component: () => import('./NextPage.vue'),
delay: 200,
timeout: 3000,
error: ErrorComponent,
loading: LoadingComponent
};
2
3
4
5
6
7
不過自從 Vue 3.x 開始,新增了 defineAsyncComponent
這個特殊的功能函式來完成這個功能:
const app = Vue.createApp({});
// 接受 Promise 建構式生成的新元件
const AsyncComp = Vue.defineAsyncComponent(
() =>
new Promise((resolve, reject) => {
resolve({
template: '<div>I am async!</div>'
})
})
);
app.component('async-example', AsyncComp);
2
3
4
5
6
7
8
9
10
11
12
13
這個 defineAsyncComponent
會回傳一個 Promise
函式,我們可以依照回傳的結果是 resolve
(成功) 或 reject
(失敗) 來進行對應的動作。
然後再將這個 AsyncComp
透過 app.component
註冊到 app
這個實體物件中。
或者當執行環境允許的話 (例如不管 IE 或使用 Vue CLI 開發等) ,利用大家所熟悉的 ES Module import
的語法,也能達到一樣的效果:
import { defineAsyncComponent, createApp } from 'vue';
const app = createApp({});
// 透過 ES Module 語法建立新元件
const AsyncComp = defineAsyncComponent(() => import('./components/AsyncComponent.vue'));
app.component('async-component', AsyncComp);
2
3
4
5
6
7