iT邦幫忙

2021 iThome 鐵人賽

DAY 23
0
Modern Web

不只懂 Vue 語法:Vue.js 觀念篇系列 第 23

不只懂 Vue 語法:試解釋遞迴元件的用法?

  • 分享至 

  • xImage
  •  

問題回答

遞迴元件是指同一個元件裏不斷引用自己,造成重複一層元件包著一層元件的情況,直至該元件所渲染的資料沒有滿足你設定的 v-if 資料,就代表此遞迴結束,不會再往下層產生同一個元件。此技巧常用於製作樹狀結構的 menu。

雖然能應用此技巧的情況不多,但也曾經出現在常見面試題中,因此也值得花時間學習一下。以下會再作詳細解說。

什麼是樹狀 menu?

以上是多層樹狀 menu 的示範。

本篇文章會以歌單為例子。

  • 第一層:音樂類別(K-Pop、US-Pop)
  • 第二層:歌手
  • 第三層:專輯
  • 最後一層:專輯的其中三首歌

遞迴元件的限制

  • 必須設定 name 屬性
  • 必須設定結束遞迴的條件

示範例子:歌單樹狀 menu

資料結構如下:

menu: [
    { 
        title: "K-POP",
        children: [
            { 
            title: 'BTS',
            children: [
                { 
                  title: "BE",
                  children: [
                    { 
                      title: "Life Goes On",
                    },
                    {
                      title: "Dynamite",
                    },
                    {
                      title: "Blue & Gray",
                    },
                  ],
                },
             ],
            }
        ]
    },
    {
        title: "US-POP",
        children: [
            {
                title: 'Billie Eilish',
                children: [ {...} ]
            }
        ]
    },
]

需要留意的重點:

  • 最外層沒有 titlechildren 屬性。
  • 由最外層的下一層開始,直至最內層之前,都有 titlechildren 屬性。
  • 最內層沒有 children 屬性。

因此,可以總結出:

  • 最外層的下一層才真正開始遞迴。
  • 遞迴條件是:看這個物件有沒有 children 屬性。
    • 最外層沒有 children --> 沒開始遞迴。
    • 最內層沒有 children --> 停止遞迴。

就著以上特點,先看看如何規劃整段程式碼:

黑色就是 <Menu> 元件,即是我們要遞迴的元件。以下會逐步解說做法。

第一步:印出最外層的項目

首先,建立 App.vue。在 App.vue,使用 v-for 把最外層的 title,即是音樂類別印出來。

App.vue

  <div class="menu-bg">
    <Menu v-for="item in menu" :key="item.title" :item="item" />
  </div>

所有資料

data() {
    return {
        menu: [
            {
                title: "K-POP",
                children: [{...}]
            },
            {
                title: "US-POP",
                children: [{...}]
            },
        ]
    }
}

加上一個灰色背景:

.menu-bg {
  background: #ededed;
}

Menu.vue:

<template>
  <p>
    {{ item.title }}
  </p>
</template>
export default {
  props: {
    item: {
      type: Object
    }
  }
};

第一步是把最外層印出來,並在 Menu 元件使用 item 來接收 menu 裏的 2 個物件。像是這樣:

<div>
    <!-- Menu 元件 -->
        <p> K-POP </p>
    <!-- Menu 元件 -->
        <p> US-POP </p>
</div>

結果:

第二步:把內層渲染出來

目前還不是樹狀 menu,只有把最外層印出來。舉例說,我們要把 title: K-POP 的 children 裏的資料渲染出來。做法很簡單:

  1. 在 Menu.vue 裏的 props,即是 item.children 取出 children 資料。
  2. 這時候你會發現,children 裏的每一筆資料,是由同樣的結構組成,即是同樣有 titlechildren
  3. 因此,你可以在 Menu.vue 裏,再次引用 <Menu /> 元件,即是在再次引用自己。

Menu.vue

<template>
    <div>
      <p>
        {{ item.title }}
      </p>
      <Menu v-for="item in item.children" :key="item.title" :item="item" />
    </div>
</template>

注意,在元件裏引用自己時,需要加入 name 屬性:

export default {
  // 加入 name,template 才認得
  name: 'Menu',
  props: {
    item: {
      type: Object
    }
  }
};

結果是把所有資料都渲染出來:

打開 Vue 檢查工具就一目了然:

但是,現在看來不像樹狀 menu,因為:

  • 最後一層並沒有 children 屬性,會造成渲染錯誤。
  • 子 menu 沒有 margin-left,畫面上很難分辨哪個是子 menu。
  • 沒有 toggle 的開關收合顯示內容。

第三步:加入 v-if 終止遞迴,以及加入所需的 CSS

先加入 CSS,讓整體看來像一個樹狀 menu:

<template>
  <div>
    <p>
      {{ item.title }}
    </p>
    <Menu
      v-for="item in item.children"
      :key="item.title"
      :item="item"
      class="sub-menu"
    />
  </div>
</template>
<style scoped>
    .sub-menu {
      margin-left: 30px;
    }
</style>

之後,加入 toggle 開關切換顯示子 menu 的內容:

<template>
  <div>
    <div class="title-bar">
      <p>
        {{ item.title }}
      </p>
      <a class="toggle-icon" href="#" @click.prevent="isOpen = !isOpen">
        {{ isOpen ? "-" : "+" }}
      </a>
    </div>
      <Menu
        v-show="isOpen"
        v-for="item in item.children"
        :key="item.title"
        :item="item"
        class="sub-menu"
      />
  </div>
</template>
data() {
    return {
      isOpen: false,
    };
}
.title-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 50px;
}

.toggle-icon {
  color: grey;
  text-decoration: none;
}

結果:

最後,加入 v-if 來設定終止遞迴的條件:沒有 children 屬性。

<template>
  <div>
    <div class="title-bar">
      <p>
        {{ item.title }}
      </p>
      <a class="toggle-icon" href="#" @click.prevent="isOpen = !isOpen">
        {{ isOpen ? "-" : "+" }}
      </a>
    </div>
      <template v-if="item.children">
        <Menu
          v-show="isOpen"
          v-for="item in item.children"
          :key="item.title"
          :item="item"
          class="sub-menu"
        />
      </template>
  </div>
</template>

最終結果

完整程式碼

https://codesandbox.io/s/shu-zhuang-menu-db6wu?file=/src/components/Menu.vue

總結

  • 遞迴元件是指在該元件裏不斷引用自己,常用於製作樹狀 menu。
  • 限制是要在該元件設定 name 屬性,以及設定終止遞迴的條件。

參考資料

VueJS - Recursive Components
重新認識 Vue.js - 2-2 元件之間的溝通傳遞


上一篇
不只懂 Vue 語法:為何 v-for 的 key 必須是唯一值?v-for 與 v-if 能否同時使用?
下一篇
不只懂 Vue 語法:試解釋如何使用導航守衛?
系列文
不只懂 Vue 語法:Vue.js 觀念篇31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言