今天要來說的是「列表渲染」。
列表在這邊表達的是「清單、多個資料」的意思,而我們可以使用它,像迴圈一樣的遍歷、渲染出多個元素~
(由於這個用法和 JS 有點相似,因此今天會搭配一些 JS 概念來理解哦)
先給個!今天會提到的內容:
- 定義
- 語法
- 解構用法
- 多層嵌套用法
- v-for 與物件
- v-for 與 template
- v-for 與 v-if
我們開始囉!
v-for
主要用於渲染「陣列」中的元素。
官方文件:我們可以使用 v-for 指令基於一個數組來渲染一個列表。
v-for 指令的值需要使用 item in items 形式的特殊語法,其中 items 是源數據的數組,而 item 是迭代項的別名在 v-for 塊中可以完整地訪問父作用域內的屬性和變量。
概念和 JS 的 for loop
和 forEach
有些相似:
v-for
內的變數,只能在當前的迭代範圍使用。因此我們通常會使用它來進行:多筆資料的響應式渲染。
只有一個參數,代表迭代元素的別名。
v-for="迭代元素別名 in 陣列名稱"
(別名:用來表示迭代元素的自定義名稱。)
包含迭代元素的別名和其索引值。
v-for="(迭代元素別名, 迭代元素索引值) in 陣列名稱"
// 索引值 是 非必要參數,若有的話,必須將參數們加上括號 ()
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>
我們在這做了:
letters
為響應式陣列,其中有元素 "a"
、"b"
、"c"
。<li>{{ letter }}</li>
綁定 v-for
指令 letter in letters
:會依序渲染 letters
中的元素(每個 letter
)。const letters = ["a", "b", "c"];
letters.forEach((letter, index) => {
// 渲染 <li>{{ letter }}</li>
})
// 因此渲染出:
// <li>a</li>
// <li>b</li>
// <li>c</li>
瀏覽器上呈現:
來搭配 索引值 試試看!
<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>
我們在這做了:
title
為響應式的值 Plan
。letters
為響應式陣列,其中有元素 "a"
、"b"
、"c"
。<li>{{ title }} {{ index + 1 }}:{{ letter }}</li>
綁定 v-for
指令 (letter, index) in letters
:將會依序渲染 letters
中的元素及索引值的操作。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,發現兩者差異:
由於 {{}}
中會被解析為表達式,而
undefined
或 null
,這些值會直接顯示在結果中。undefined
將被解析成字串 "undefined"
,顯示在畫面上。undefined
或 null
,會被解析為「空白」,不會影響整體字串的顯示結果。所以如果將 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>
請看!
使用字串拼接寫法 {{ title + " " + (index + 1) + ":" + letter }}
的第一個 letter
是 undefined
,所以 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
陣列中的屬性值。
瀏覽器上呈現:
如果我們使用解構賦值,將 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 作用域都可以訪問到父級作用域
這邊的「多層」發生的情境,會是在「陣列元素為物件,物件屬性又為陣列」的時候發生。
當我們有一個響應式陣列,它的元素是物件、物件屬性又是陣列,長這樣:
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>
來解釋一下:
<div v-for="item in items"></div>
:items
陣列中的每一個物件。item
是:{ name: "a", data: ["a1", "a2", "a3"] }
。<li v-for="innerItem in item.data">{{ item.name }}:{{ innerItem }}</li>
:item
(1.)其中 data
屬性裡的每個元素。innerItem
是:"a1"
。{{ item.name }}:{{ innerItem }}
:// 流程會是依外層渲染到內層
迭代第一個物件:
渲染第一個元素 >> `a:a1`
渲染第二個元素 >> `a:a2`
渲染第三個元素 >> `a:a3`
迭代第二個物件:
渲染第一個元素 >> `b:b1`
渲染第二個元素 >> `b:b2`
渲染第三個元素 >> `b:b3`
迭代第三個物件:
渲染第一個元素 >> `c:c1`
渲染第二個元素 >> `c:c2`
渲染第三個元素 >> `c:c3`
我們看一下瀏覽器上呈現的結構:
我們來改寫剛剛上方 解構用法範例 的樣子:
將響應式陣列裡面包物件試試看~
<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
為一個響應式的陣列,它包含了兩個物件,其內有 name
和 songs
(陣列)。
我們直接看模板內的操作:
<li v-for="(band, index) in bandList">
{{ title }} {{ index + 1 }}:{{ band.name }}
<ul v-for="song in band.songs">
歌曲:{{ song }}
</ul>
</li>
在外、內層分別都有定義文本插值,我們可以理解一下它們的渲染順序及結構:
bandList
的元素 band
,並且渲染 {{ title }} {{ index + 1 }}:{{ bandList.name }}
,然後進入內層迭代。{ name: "溫蒂漫步", songs: ["Spring Spring", "我想和你一起", "艾菲號"] }
這個物件時,會進入內層繼續迭代 <ul>
中的 歌曲:{{ song }}
。["Spring Spring", "我想和你一起", "艾菲號"]
。瀏覽器上的結果:
(突然變成樂團版✨)
官方文件:你也可以使用 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()
確認一下迭代順序:
因此,如果使用 v-for
語法對物件屬性做渲染,我們預期會先迭代 name
、然後是 data
。
然後我們定義一下模板:
<template>
<div v-for="(value, key) in myObj">
{{ key }}:{{ value }}
</div>
</template>
因此將會 name
會先渲染 {{ key }}:{{ value }}
,然後才是 data
渲染 {{ key }}:{{ value }}
。
確認看看瀏覽器上的呈現:
(關於 index
在這邊的用法就沒有做範例了,使用方式會和上方範例一的用法概念相同喔)
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>
,會依序渲染:
由於兩者同時存在時,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
中的元素,並且將 status
為 false
的渲染出來。
但因為 v-for
和 v-if
在同一層時,會先執行 v-if
,而 v-if
表達式定義的 todo
尚未被宣告,就會拋出 not defined
的錯誤:
解決方案是:我們可以把 v-for
用 <template>
包在 v-if
外層:
<template v-for="todo in todoList">
<li v-if="!todo.status">
{{ todo.title }}
</li>
</template>
就不會報錯囉!
成功看到還沒做完的事情了⋯⋯(怎麼會醬⋯⋯)
這邊可以注意一個點:由於 v-if
優先級高於 v-for
,所以 v-for
目前遍歷的就是只有「條件成立的元素」。
因此 v-for
直接跳過了 v-if
條件不成立的元素們!
(感覺在資料很龐大的時候就很有差別耶)
今天帶大家認識 v-for
這位很會列資料的同事(底細真多⋯⋯腦袋爆炸)
明天會帶大家來認識它的小夥伴 key
~
就這樣經過了 2/3~
(我的腎上腺素:走一步算一步)
明天見惹各位~~~
https://github.com/Jamixcs/2024iThome-jamixcs/tree/main/src/components/day20