# 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