iT邦幫忙

2024 iThome 鐵人賽

DAY 20
0
Modern Web

欸你是要進 Vue 了沒?系列 第 20

欸你是要進 Vue 了沒? - Day20:Vue 列表渲染之那個很會幫主管列資料的 v-for 同事

  • 分享至 

  • xImage
  •  

今天要來說的是「列表渲染」。
列表在這邊表達的是「清單、多個資料」的意思,而我們可以使用它,像迴圈一樣的遍歷、渲染出多個元素~
(由於這個用法和 JS 有點相似,因此今天會搭配一些 JS 概念來理解哦)

先給個!今天會提到的內容:

  • 定義
  • 語法
  • 解構用法
  • 多層嵌套用法
  • v-for 與物件
  • v-for 與 template
  • v-for 與 v-if

我們開始囉!
/images/emoticon/emoticon76.gif/images/emoticon/emoticon76.gif

定義

v-for 主要用於渲染「陣列」中的元素。

官方文件:我們可以使用 v-for 指令基於一個數組來渲染一個列表。
v-for 指令的值需要使用 item in items 形式的特殊語法,其中 items 是源數據的數組,而 item 是迭代項的別名

在 v-for 塊中可以完整地訪問父作用域內的屬性和變量。

概念和 JS 的 for loopforEach 有些相似:

  • 可以進行陣列元素遍歷。
  • 可以取得父層的變數。
  • 宣告在 v-for 內的變數,只能在當前的迭代範圍使用。

因此我們通常會使用它來進行:多筆資料的響應式渲染。

語法

語法一:單一參數

只有一個參數,代表迭代元素的別名。

v-for="迭代元素別名 in 陣列名稱"

(別名:用來表示迭代元素的自定義名稱。)

語法二:兩個參數

包含迭代元素的別名和其索引值。

v-for="(迭代元素別名, 迭代元素索引值) in 陣列名稱"
// 索引值 是 非必要參數,若有的話,必須將參數們加上括號 ()

用 forEach 思考~

v-for 遍歷的行為就和 forEach 迭代陣列中的元素一樣。
所以我們可以這麼想:

const 陣列名稱 = 定義的陣列;

陣列名稱.forEach((迭代元素別名, 迭代元素索引值) => {
  // 會渲染相應部分
});

單一參數範例

我們先以 僅使用元素別名 的例子看看:

<script setup>
import { ref } from "vue";

const letters = ref(["a", "b", "c"]);
</script>
<template>
  <li v-for="letter in letters">{{ letter }}</li>
</template>

我們在這做了:

  1. 定義 letters 為響應式陣列,其中有元素 "a""b""c"
  2. 模板中 <li>{{ letter }}</li> 綁定 v-for 指令 letter in letters:會依序渲染 letters 中的元素(每個 letter)。

配合 forEach 思考~

const letters = ["a", "b", "c"];

letters.forEach((letter, index) => {
  // 渲染 <li>{{ letter }}</li>
})

// 因此渲染出:
// <li>a</li>
// <li>b</li>
// <li>c</li>

瀏覽器上呈現:
https://ithelp.ithome.com.tw/upload/images/20241003/20169139HWcTE8OppG.png

兩個參數範例

來搭配 索引值 試試看!

<script setup>
import { ref } from "vue";

const title = ref("Plan");
const letters = ref(["a", "b", "c"]);
</script>
<template>
  <li v-for="(letter, index) in letters">
    {{ title }} {{ index + 1 }}:{{ letter }}
  </li>
</template>

我們在這做了:

  1. 定義 title 為響應式的值 Plan
  2. 定義 letters 為響應式陣列,其中有元素 "a""b""c"
  3. 模板中 <li>{{ title }} {{ index + 1 }}:{{ letter }}</li> 綁定 v-for 指令 (letter, index) in letters:將會依序渲染 letters 中的元素及索引值的操作。

配合 forEach 思考~

const title = "Plan";
const letters = ["a", "b", "c"];

letters.forEach((letter, index) => {
  // 渲染 <li>{{ title }} {{ index + 1 }}:{{ letter }}</li>
});

// 因此渲染出:
// <li>Plan 1:a</li>
// <li>Plan 2:b</li>
// <li>Plan 3:c</li>

插播踩坑小分享:字串拼接寫法

原本我在模板中寫的是「字串拼接」的方法:

<template>
  <li v-for="(letter, index) in letters">
    {{ title + " " + (index + 1) + ":" + letter }}
  </li>
</template>

渲染的結果和上方範例,全使用插值是一樣的,去詢問了 GPT,發現兩者差異:
由於 {{}} 中會被解析為表達式,而

  • 字串拼接的寫法,會將所有變數和字串「強制轉換為字串」來拼接。
    如果變數是 undefinednull,這些值會直接顯示在結果中。
    例如,undefined 將被解析成字串 "undefined",顯示在畫面上。
  • 模板插值中的每個變數被「單獨解析」。
    如果變數是 undefinednull,會被解析為「空白」,不會影響整體字串的顯示結果。

所以如果將 letters 其一元素定義為 undefined

<script setup>
import { ref } from "vue";

const title = ref("Plan");
const letters = ref([undefined, "b", "c"]);
</script>
<template>
  <!-- 字串拼接 -->
  <li v-for="(letter, index) in letters" :key="index">
    {{ title + " " + (index + 1) + ":" + letter }}
  </li>
  <!-- 插值 -->
  <li v-for="(letter, index) in letters" :key="index">
    {{ title }} {{ index + 1 }}:{{ letter }}
  </li>
</template>

請看!
https://ithelp.ithome.com.tw/upload/images/20241003/20169139OGe55UWAgM.png
使用字串拼接寫法 {{ title + " " + (index + 1) + ":" + letter }} 的第一個 letterundefined,所以 undefined 就被強制轉為字串並顯示了。

解構用法

當陣列裡的元素為「物件」時,我們可以使用「解構賦值」,直接取得元素屬性的值。

我們一起來看看未解構和使用解構之間的差異。

未解構範例

<script setup>
import { ref } from "vue";

const title = ref("樂團");
const bands = ref([{ name: "溫蒂漫步" }, { name: "甜約翰" }]);
</script>
<template>
  <li v-for="(band, index) in bands">
    {{ title }} {{ index + 1 }}:{{ bands.name }}
  </li>
</template>

模板中需用 bands.name 取得 bands 陣列中的屬性值。

瀏覽器上呈現:
https://ithelp.ithome.com.tw/upload/images/20241003/20169139HZxDHMVi7F.png

解構範例

如果我們使用解構賦值,將 bands 中的 name 解構:

<script setup>
import { ref } from "vue";

const title = ref("樂團");
const bands = ref([{ name: "溫蒂漫步" }, { name: "甜約翰" }]);
</script>
<template>
  // 從 bands 中直接取 name 的值 
  <li v-for="({ name }, index) in bands">
    {{ band }} {{ index + 1 }}:{{ name }}
  </li>
</template>

{ name } 進行解構,將 band 解構為 name,可以在模板語法中直接用 name 變數使用。

如同對每個 band 做了這件事:

bands.forEach((band, index) => {
  const name = band.name;
});

多層嵌套用法

v-for 可以多層、巢狀使用,內層皆可以取得到外層的變數。

官方文件:作用域的工作方式和函數的作用域很類似。每個 v-for 作用域都可以訪問到父級作用域

這邊的「多層」發生的情境,會是在「陣列元素為物件,物件屬性又為陣列」的時候發生。

用 forEach 思考~

當我們有一個響應式陣列,它的元素是物件、物件屬性又是陣列,長這樣:

const items = ref([
  { name: "a", data: ["a1", "a2", "a3"] },
  { name: "b", data: ["b1", "b2", "b3"] },
  { name: "c", data: ["c1", "c2", "c3"] },
]);

當我們想要遍歷 data 屬性其中的陣列元素時,像是,我們想要印出這樣的資料:

a:"a1"
a:"a2"
a:"a3"
b:"b1"
b:"b2"
b:"b3"
c:"c1"
c:"c2"
c:"c3"

我們會這樣做:

const items = [
  { name: "a", data: ["a1", "a2", "a3"] },
  { name: "b", data: ["b1", "b2", "b3"] },
  { name: "c", data: ["c1", "c2", "c3"] },
];

items.forEach((item) => {
  item.data.forEach((innerItem) => {
    console.log(`${item.name}:${innerItem}`);
   // 這裡可以使用外層的 item 變數
  });
});

有感受到巢狀和使用變數的作用域的感覺了嗎!

回到 v-for,我們可以這麼做:

<script setup>
import { ref } from "vue";

const items = ref([
  { name: "a", data: ["a1", "a2", "a3"] },
  { name: "b", data: ["b1", "b2", "b3"] },
  { name: "c", data: ["c1", "c2", "c3"] },
]);
</script>
<template>
  <div v-for="item in items">
    <li v-for="innerItem in item.data">{{ item.name }}:{{ innerItem }}</li> // 這裡可以使用外層的 item 變數
  </div>
</template>

來解釋一下:

  1. 外層 <div v-for="item in items"></div>
    會迭代 items 陣列中的每一個物件。
    例如,第一個迭代的 item 是:{ name: "a", data: ["a1", "a2", "a3"] }
  2. 內層 <li v-for="innerItem in item.data">{{ item.name }}:{{ innerItem }}</li>
    會迭代 item(1.)其中 data 屬性裡的每個元素。
    例如,第一個迭代的 innerItem 是:"a1"
  3. 渲染內容 {{ item.name }}:{{ innerItem }}
// 流程會是依外層渲染到內層
迭代第一個物件:
渲染第一個元素 >> `a:a1`
渲染第二個元素 >> `a:a2`
渲染第三個元素 >> `a:a3`

迭代第二個物件:
渲染第一個元素 >> `b:b1`
渲染第二個元素 >> `b:b2`
渲染第三個元素 >> `b:b3`

迭代第三個物件:
渲染第一個元素 >> `c:c1`
渲染第二個元素 >> `c:c2`
渲染第三個元素 >> `c:c3`

我們看一下瀏覽器上呈現的結構:
https://ithelp.ithome.com.tw/upload/images/20241003/20169139QIyPcGsXkn.png

範例

我們來改寫剛剛上方 解構用法範例 的樣子:
將響應式陣列裡面包物件試試看~

<script setup>
import { ref } from "vue";

const title = ref("樂團");
const bandList = ref([
  { name: "溫蒂漫步", songs: ["Spring Spring", "我想和你一起", "艾菲號"] },
  { name: "甜約翰", songs: ["降雨機率", "城市的浪漫運作", "留給你的我從未"] },
]);
</script>
<template>
  <li v-for="(band, index) in bandList">
    {{ title }} {{ index + 1 }}:{{ bandList.name }}
    <ul v-for="song in band.songs">
      歌曲:{{
        song
      }}
    </ul>
  </li>
</template>

這邊定義 bandList 為一個響應式的陣列,它包含了兩個物件,其內有 namesongs(陣列)。

我們直接看模板內的操作:

<li v-for="(band, index) in bandList">
  {{ title }} {{ index + 1 }}:{{ band.name }}
 <ul v-for="song in band.songs">
  歌曲:{{ song }}
  </ul>
</li>

在外、內層分別都有定義文本插值,我們可以理解一下它們的渲染順序及結構:

  1. 外層會迭代 bandList 的元素 band,並且渲染 {{ title }} {{ index + 1 }}:{{ bandList.name }},然後進入內層迭代。
  • 例如:外層第一次迭代到 { name: "溫蒂漫步", songs: ["Spring Spring", "我想和你一起", "艾菲號"] } 這個物件時,會進入內層繼續迭代 <ul> 中的 歌曲:{{ song }}
    依序顯示 ["Spring Spring", "我想和你一起", "艾菲號"]
  1. 內層迭代結束後,進入外層下一次迭代。

瀏覽器上的結果:
https://ithelp.ithome.com.tw/upload/images/20241003/20169139UtZRTPC0rd.png

(突然變成樂團版

v-for 與物件

官方文件:你也可以使用 v-for 來遍歷一個對象的所有屬性。遍歷的順序會基於對該對象調用 Object.keys() 的返回值來決定。

也就是說:我們可以直接一次渲染出「物件」中的「所有」屬性(渲染順序會是依照物件屬性的默認順序)。

語法

v-for="(物件 value 別名, 物件 key 別名, 迭代元素索引值) in 自定義物件"
// 後兩個參數是非必要的

範例

我們先宣告一個響應式物件!

<script setup>
import { reactive } from "vue";

const myObj = reactive({ name: "a", data: ["a1", "a2", "a3"] });
</script>

Object.keys() 確認一下迭代順序:
https://ithelp.ithome.com.tw/upload/images/20241003/20169139d0HyjtAm8W.png
因此,如果使用 v-for 語法對物件屬性做渲染,我們預期會先迭代 name、然後是 data

然後我們定義一下模板:

<template>
  <div v-for="(value, key) in myObj">
    {{ key }}:{{ value }}
  </div>
</template>

因此將會 name 會先渲染 {{ key }}:{{ value }},然後才是 data 渲染 {{ key }}:{{ value }}

確認看看瀏覽器上的呈現:
https://ithelp.ithome.com.tw/upload/images/20241003/20169139UFAfqLGRpP.png

(關於 index 在這邊的用法就沒有做範例了,使用方式會和上方範例一的用法概念相同喔)

v-for 與 template

v-for 可以直接用在控制迭代 <template><template> 中可以包含「多個元素」。

範例

可以這麼寫:

<script setup>
import { ref } from "vue";

const items = ref(["a", "b", "c"]);
</script>
<template>
  <template v-for="item in items">
    <li>{{ item }}</li>
    <div class="divider"></div>
  </template>
</template>
<style>
.divider {
  border-bottom: 10px rgb(116, 184, 140) solid;
}
</style>

其中 v-for 控制的 template<li><div>,會依序渲染:
https://ithelp.ithome.com.tw/upload/images/20241003/20169139FLMYQKeZ89.png

v-if 與 v-for

官方不推薦同時使用

由於兩者同時存在時,v-if 會首先被執行。

因此我們如果這樣寫:

<script setup>
import { ref } from "vue";

const todoList = ref([
  { title: "散步", status: true },
  { title: "聽音樂", status: true },
  { title: "練鋼琴", status: true },
  { title: "寫鐵人賽", status: false },
]);
</script>
<template>
  <li v-for="todo in todoList" v-if="!todo.status">
    {{ todo.title }}
  </li>
</template>

原先我們期望做到的是:遍歷 todoList 中的元素,並且將 statusfalse 的渲染出來。

但因為 v-forv-if 在同一層時,會先執行 v-if,而 v-if 表達式定義的 todo 尚未被宣告,就會拋出 not defined 的錯誤:
https://ithelp.ithome.com.tw/upload/images/20241003/201691395xCc9G7k9B.png

同時使用的解決方案

解決方案是:我們可以把 v-for<template> 包在 v-if 外層:

<template v-for="todo in todoList">
  <li v-if="!todo.status">
    {{ todo.title }}
  </li>
</template>

就不會報錯囉!
成功看到還沒做完的事情了⋯⋯(怎麼會醬⋯⋯)
https://ithelp.ithome.com.tw/upload/images/20241003/201691399Z6sVhdrdG.png

v-for 在 v-if 外層的優點就是效能提升!

這邊可以注意一個點:由於 v-if 優先級高於 v-for,所以 v-for 目前遍歷的就是只有「條件成立的元素」。
因此 v-for 直接跳過了 v-if 條件不成立的元素們!
(感覺在資料很龐大的時候就很有差別耶)

小結

今天帶大家認識 v-for 這位很會列資料的同事(底細真多⋯⋯腦袋爆炸)
明天會帶大家來認識它的小夥伴 key

就這樣經過了 2/3~
(我的腎上腺素:走一步算一步)

明天見惹各位~~~/images/emoticon/emoticon29.gif

範例 code ⬇️

https://github.com/Jamixcs/2024iThome-jamixcs/tree/main/src/components/day20

參考資料


上一篇
欸你是要進 Vue 了沒? - Day19:Vue 條件渲染家族 v-if、v-else-if、v-else、v-show
下一篇
欸你是要進 Vue 了沒? - Day21:Vue 列表渲染之 v-for 和它の關鍵小夥伴 key && 虛擬 DOM
系列文
欸你是要進 Vue 了沒?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言