iT邦幫忙

2022 iThome 鐵人賽

DAY 9
0
Modern Web

真的好想離開 Vue 3 新手村 feat. CompositionAPI系列 第 9

真的好想離開 Vue 3 新手村 - Day 9: v-for 與他的坑 feat. key & v-if

  • 分享至 

  • xImage
  •  

Outline

  • 語法簡介
  • v-for 與 key 的關係
    • 為什麼要綁定 key
    • 什麼狀況下不能用 index 作為 key
  • v-if & v-for
  • 偷看一下原始碼

語法簡介

Syntax

v-for="item in items"
v-for="item of items"

在 Vue 模板中,可以用 v-for 指令,來迭代資料,並根據資料渲染重複的 HTML 標籤或自訂元件。

v-for 指令可以接收的 value 類型包含:

  • 數字
  • 字串
  • 陣列及其他可迭代物件(Set, Map)
  • 物件

1. 數字

<li v-for="number in 5" :key="number">{{ number }}</li>

數字需為正整數,並且一定從 1 開始,範圍為 1 ~ number,不支援其他規則的數列(如:只要偶數或奇數)。

註:給負數或小數會噴錯,噴錯的內容是「無效的陣列長度」(RangeError: Invalid array length),這是因為當輸入的內容是數字時,Vue 會根據輸入的數字,去創造一個長度為該數字的陣列,以此進行 list rendering。

2. 字串

<li v-for="character in 'string'" :key="character">{{ item }}</li>
<li v-for="(character, index) in 'string'" :key="character">{{ character }}</li>

如果是有先看完文件的人,就會知道文件沒有特別提到: v-for 可以接收字串型別的資料(我也是意外發現的)。
當傳入的資料為字串,會將字串轉為陣列進行迭代,可以在渲染時拿到字串的每個字符和 index。

3. 陣列及其他可迭代物件

當接收的資料為可迭代物件,包含陣列、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>

4. 物件

當接收的資料為一般物件,在渲染時可以拿到物件中每個 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 的規範,排序規則如下:

  1. 先找整數型別索引,由小排到大
  2. 按照建立順序,越早定義則排序在前
  3. 最後加入 Symbol 型別的 key

但實作細節還是要看瀏覽器遵循的版本為何,如採用舊版本的規範,定義可能不明確。
一般按照建立順序進行迭代是沒問題的,大家不會在一個物件裡,混雜著使用數字跟字串作為 key 吧... 不會吧...

補充

基本上,在 v-for 處理 list render 的時候,會將收到的 value (資料) 轉為陣列來讀取並進行渲染,可以看 renderList 的實作程式碼:core/packages/runtime-core/src/helpers/renderList.ts

巢狀 v-for

內層的 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 的關係

在使用 v-for 做渲染的時候,如果沒有提供 key 這個屬性,eslint 會有毛毛蟲警告,但網頁還是可以正常運作,console 也不會噴錯或報警告。

所以 key 的用途到底是什麼?
這和 Vue 更新畫面的機制有關。

Vue 的畫面更新機制

Vue 為了提升性能採取 "in-place patch" 策略,簡單來說:
Vue 預設會最小化「更新」的工作量,盡可能的重複使用元素(或說節點),最小化移動 DOM 元素,只更新相對應的節點內容。

但是在下列情況不太適合這種更新方式:

  1. v-for 渲染內容和子元件狀態有關
  2. v-for 渲染內容和暫時性的 DOM 狀態有關,例如:form input values

舉個例子(範例 code)

用 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>

key 屬性

期望值: number | string | symbol

  • key 是 Vue 內建的特殊屬性
  • 為了要比對更新前後的 DOM(virtual DOM)時,去辨識節點用的,要用不會改變的唯一值
  • index 會有潛在風險,不適合用在陣列會變換順序的情況
  • 用基本型別,不要用 object 或 array (比較的變成 reference)

沒有 key 的情況下,Vue 會在最小化移動元素的情況下來更新,有 key 的情況下, Vue 會根據 key 的順序,重新排列(reorder)元素,如果 key 不存在的話,那個元素就會被銷毀。

為什麼不能用 index

index 對於陣列來說,確實是唯一值,但是會因為操作陣列順序而有變動不保證會永遠指向同一筆資料,所以對有機會被重新排序的資料來說,index 不適合拿來作為 v-for 渲染的 key 值,會讓 Vue 在比較新舊節點時,出現非預期的情況。

以剛剛的例子來說:如果在第一行 apple 的輸入框中輸入數字,'apple' 的 index 本來是 0,重新排序後變成 1,但輸入框的內容會一直跟著 index0 的那筆資料,也就失去給予 key 值的意義。

大家可以試著用剛剛的範例,幫 v-for 加上index 作為 key,還是會看到一樣的畫面:

也可以看看這個影片,講得很清楚(謝謝Sherry分享)

v-if 與 v-for

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,所以會報錯。

解決方式

  1. 善用 <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>
  1. 在 template 對 v-for 陣列做 filter
<li
  v-for="{ name } in todos.filter((todo) => todo.isComplete === false)"
  :key="name">
  {{ name }}
</li>
  1. 在 script 使用 computed
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>

總結

  • v-for
    • 用途:在 Vue 模板中,可以用 v-for 指令,來迭代資料,並根據資料渲染重複的 HTML 標籤或自訂元件。
    • 可接收資料:正整數、字串、陣列、物件。
    • 可以使用巢狀 v-for,內層的 v-for 可以拿到外層 v-for 的資料。
  • v-for 注意事項
    • 強烈建議綁定 key 屬性,並給予不會變動的唯一值,告訴 Vue 整個渲染的元素要跟著 key 值相同的資料一起移動,避免 Vue 最小化更新("in-place patch" 策略)
    • 不能和 v-if 用在同一個元素或元件上,因為會優先判別 v-if,此時取不到 v-for 的資料

參考資料


上一篇
真的好想離開 Vue 3 新手村 - Day 8: 認識 Vue directive 和 v-if v.s v-show
下一篇
真的好想離開 Vue 3 新手村 - Day 10: 從原生 JS 理解 Vue 3 響應式基礎 - reactive & ref (上)
系列文
真的好想離開 Vue 3 新手村 feat. CompositionAPI31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言