# 2-3 動態元件管理

前面我們曾介紹 Vue.js 拆分元件的方式,也提到拆分後的元件可以透過 v-ifv-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>
1
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');
1
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() }`;
    }
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13

然後用一個 <component> 來取代 <tab-home><tab-posts><tab-archive>

  <component :is="currentTabComponent"></component>
1

像這樣,我們只需要透過 <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' })
});
1
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>
1
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>
1
2
3
4

不過要注意的是,由於 <keep-alive> 同時間只會有一個子元件會被渲染,且也要避免 v-ifv-for 共同使用造成的問題。

# includeexcludemax 屬性

要是切換的子元件數量太多,每個都加上 keep-alive 必定會造成某種程度上的效能成本。

若我們只想針對某部分的子元件進行暫存快取 (或排除某些子元件不要 keep-alive), 則可以透過 <keep-alive> 提供的 includeexclude 屬性來處理,如:


 



<!-- 只保留 tab-home 與 tab-posts 的狀態 -->
<keep-alive include="tab-home,tab-posts">
  <component :is="currentTabComponent"></component>
</keep-alive>
1
2
3
4

需要注意的是,若使用 includeexclude 屬性時,子元件需要加上 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' })
});
1
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>
1
2
3
4
5
6
7
8
9

當然 exclude 屬性的用法也是一樣,差別只在一個是列入,另一個是排除。

小提醒

includeexclude 對應的條件為子元件的 name 屬性,而不是子元件的標籤名。

像是:

app.component('my-component', {
  name: 'Home-Component'
});
1
2
3

那麼在使用 include/exclude 的時候就必須寫成 :include="'Home-Component'" 而不是 'my-component'

另外,為了避免元件數量過多造成的性能浪費, <keep-alive> 也提供了 max 屬性來提供開發者指定瀏覽器暫存的元件數量:

<keep-alive :max="2">
  <component v-bind:is="currentTabComponent" />
</keep-alive>
1
2
3

此時,我們就可以透過 :max 來指定暫存的子元件數量, <keep-alive> 只會保留最後引入的兩個子元件狀態。 而 :max 也可與前面的 :include:exclude 搭配使用,提供了開發上的靈活性。

<keep-alive> 標籤包覆後的元件,即使在切換前修改了資料,再次切換回來時仍能保有原來的狀態。 像此類元件其實就如同我們在 2-1 小節 元件系統的特性 所提到的 「功能型元件」那樣的特殊功能來使用。

# 特殊的生命週期 Hooks: activateddeactivated

與其他元件不同的是, Vue.js 擴充了兩組 Hooks function 給 <keep-alive> 來使用,分別是 activateddeactivated 這兩個 lifecycle hook。

在介紹這兩個 hook 前,讓我們改寫一下前面的例子, 分別在 createdmounted 以及 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.`);
}
1
2
3
4
5
6
7
8
9
10

當元件在動態切換的時候,若是沒有加上 <keep-alive> 標籤,則會在切換元件時重新觸發元件的各種生命週期階段。

元件建立時,如同我們在前面介紹過的,會先從 created 開始,然後是 mounted 階段。 而切換元件時的執行順序是 「建立新的元件」(created) → 「銷毀目前元件」(unmounted) → 「掛載新的元件」(mounted)。

試一試

現在,讓我們再加回 <keep-alive> 標籤,並且加上 activateddeactivated 這兩個 hook (原本的 unmounted 未移除) :

activated () {
  this.$emit('update', `${this.$options.name} Activated.`);
},
deactivated () {
  this.$emit('update', `${this.$options.name} Deactivated.`);
},
1
2
3
4
5
6
試一試

此時若在加入 <keep-alive> 的情況下,在元件首次建立時,就會先進行 createdmountedactivated 三個階段, 而當我們切換至新的元件時,則會依序執行:

「建立新的元件」(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
};
1
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);
1
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);
1
2
3
4
5
6
7
Last Updated: 12/28/2020, 7:34:16 PM