index
作為 keyv-if
& v-for
Syntax
v-for="item in items"
v-for="item of items"
在 Vue 模板中,可以用 v-for 指令,來迭代資料,並根據資料渲染重複的 HTML 標籤或自訂元件。
v-for 指令可以接收的 value
類型包含:
<li v-for="number in 5" :key="number">{{ number }}</li>
數字需為正整數,並且一定從 1 開始,範圍為 1 ~ number,不支援其他規則的數列(如:只要偶數或奇數)。
註:給負數或小數會噴錯,噴錯的內容是「無效的陣列長度」(RangeError: Invalid array length),這是因為當輸入的內容是數字時,Vue 會根據輸入的數字,去創造一個長度為該數字的陣列,以此進行 list rendering。
<li v-for="character in 'string'" :key="character">{{ item }}</li>
<li v-for="(character, index) in 'string'" :key="character">{{ character }}</li>
如果是有先看完文件的人,就會知道文件沒有特別提到: v-for 可以接收字串型別的資料(我也是意外發現的)。
當傳入的資料為字串,會將字串轉為陣列進行迭代,可以在渲染時拿到字串的每個字符和 index。
當接收的資料為可迭代物件,包含陣列、Set 和 Map,在渲染時,可以拿到陣列中每個資料的 value 和 index。
<li v-for="item in items" :key="item">{{ item }}</li>
<li v-for="(value, index) in items" :key="value">{{ value }}</li>
如果陣列內容為物件,支援直接解構,讓 template 看起來更乾淨。
const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
<li v-for="{ message } in items" :key="message">{{ message }}</li>
當接收的資料為一般物件,在渲染時可以拿到物件中每個 key、value 和 index。
<li v-for="item in items" :key="item">{{ item }}</li>
<li v-for="(key, value, index) in items" :key="value">{{ value }}</li>
文件提到,迭代順序是根據將物件丟進 Objecy.keys()
內回傳的結果,那透過 Objecy.keys()
得到的迭代順序為何?
來點原生 JavaScript 吧!
一般物件是不能進行迭代的,所以 Vue 是透過 Objecy.keys()
來迭代一般物件。
按照 ES6 的規範,排序規則如下:
但實作細節還是要看瀏覽器遵循的版本為何,如採用舊版本的規範,定義可能不明確。
一般按照建立順序進行迭代是沒問題的,大家不會在一個物件裡,混雜著使用數字跟字串作為 key 吧... 不會吧...
基本上,在 v-for 處理 list render 的時候,會將收到的 value
(資料) 轉為陣列來讀取並進行渲染,可以看 renderList 的實作程式碼:core/packages/runtime-core/src/helpers/renderList.ts
內層的 v-for 可以拿到外層 v-for 的資料,對巢狀資料進行渲染很方便。
const nestedData = ref({
product: {
M1: "Macbook Air M1 2020",
M2: "Macbook Air M2 2022",
},
retina: {
M1: "13.3 吋",
M2: "13.6 吋",
},
chips: {
M1: "Apple M1 晶片",
M2: "Apple M2 晶片",
},
});
<div class="wrapper">
<div
v-for="(categoryContent, categoryName) in nestedData"
:key="categoryName"
class="cell"
>
<p>{{ categoryName }}</p>
<div v-for="(content, chip) in categoryContent" :key="content">
<p>{{ content }}</p>
</div>
</div>
</div>
在使用 v-for
做渲染的時候,如果沒有提供 key
這個屬性,eslint 會有毛毛蟲警告,但網頁還是可以正常運作,console 也不會噴錯或報警告。
所以 key 的用途到底是什麼?
這和 Vue 更新畫面的機制有關。
Vue 為了提升性能採取 "in-place patch" 策略,簡單來說:
Vue 預設會最小化「更新」的工作量,盡可能的重複使用元素(或說節點),最小化移動 DOM 元素,只更新相對應的節點內容。
但是在下列情況不太適合這種更新方式:
用 v-for 渲染一組 HTML 結構如下:
import { ref } from 'vue'
const fruits = ref(['apple', 'banana', 'kiwi'])
//按鈕綁定一個改變陣列順序的 method
function reorder() {
fruits.value = [fruits.value[1], fruits.value[2], fruits.value[0]]
}
<template>
<div class="wrapper">
<div @click="reorder" class="button">重新排序</div>
<div v-for="(name, index) in fruits" class="fruits" >
<span>{{ name }}</span>
<input type="number" />
</div>
</div>
</template>
我並沒有綁定 key
值,還是可以成功渲染,但每次重新排序的時候,<input>
輸入框的內容卻沒有跟著新的陣列一起移動。
這是因為 Vue 更新畫面時,只有比對到 <span>
裡面內容({{name}}
)的差異,所以就複用了 <div class="fruit"></div>
這個元素,只根據新的陣列順序更新了 <span>
。
給 v-for 渲染的元素綁定 key
值是為了幫助 Vue 去確認元素/節點的身份,以上面的範例來說,就是告訴 Vue 不要在這裡複用元件,整個 <div class="fruit"></div>
要跟著 key
值為 name
的資料一起被更新移動!
<div v-for="(name, index) in fruits" class="fruits" :key="name">
<span>{{ name }}</span>
<input type="number" />
</div>
期望值: number | string | symbol
index
會有潛在風險,不適合用在陣列會變換順序的情況沒有 key 的情況下,Vue 會在最小化移動元素的情況下來更新,有 key 的情況下, Vue 會根據 key 的順序,重新排列(reorder)元素,如果 key 不存在的話,那個元素就會被銷毀。
index 對於陣列來說,確實是唯一值,但是會因為操作陣列順序而有變動,不保證會永遠指向同一筆資料,所以對有機會被重新排序的資料來說,index 不適合拿來作為 v-for 渲染的 key 值,會讓 Vue 在比較新舊節點時,出現非預期的情況。
以剛剛的例子來說:如果在第一行 apple 的輸入框中輸入數字,'apple'
的 index 本來是 0
,重新排序後變成 1
,但輸入框的內容會一直跟著 index
為 0
的那筆資料,也就失去給予 key 值的意義。
大家可以試著用剛剛的範例,幫 v-for 加上index
作為 key
,還是會看到一樣的畫面:
v-if 和 v-for 不能用在同一個元素上,因為 v-if 無法使用 v-for 中的變數。
先試想會將 v-if 和 v-for 放在同一個元素上的使用情境:我們有一個 todo 清單,想要渲染出清單中尚未完成的項目。
const todos = [
{ name: "買菜", isComplete: true },
{ name: "看牙醫", isComplete: false },
{ name: "寫文章", isComplete: false },
];
<li v-for="todo in todos"v-if="!todo.isComplete">
{{ todo.name }}
</li>
會得到下面的報錯訊息:
Uncaught TypeError: Cannot read properties of undefined (reading ‘isComplete’)
在解析 template 時,會先判斷 v-if 再處理 v-for 指令,當 v-if 判斷的變數來自 v-for 時,因為讀取不到 v-for 指令的資料,todo 被視為 undefined,所以會報錯。
<template>
分開 v-for、v-if<template v-for="todo in todos">
<li v-if="!todo.isComplete">
{{ todo.name }}
</li>
</template>
陣列內的項目為物件,不要忘記可以直接解構,就不用每次都用 .propertyName
去拿資料。
<template v-for="{ name, isComplete } in todos">
<li v-if="!isComplete" :key="name">
{{ name }}
</li>
</template>
<li
v-for="{ name } in todos.filter((todo) => todo.isComplete === false)"
:key="name">
{{ name }}
</li>
const todos = [
{ name: "買菜", isComplete: false },
{ name: "看牙醫", isComplete: false },
{ name: "寫文章", isComplete: false },
];
const undos = computed(() => todos.filter((todo) => todo.isComplete === false));
<li v-for="{ name } in undos" :key="name">{{ name }}</li>
key
屬性,並給予不會變動的唯一值,告訴 Vue 整個渲染的元素要跟著 key
值相同的資料一起移動,避免 Vue 最小化更新("in-place patch" 策略)