跳至主要內容
Skip to content

Vue 中 key 的作用

key 是 Vue 面試裡非常常見的題目。它看起來只是 v-for 裡的一個屬性,但背後其實牽涉到 VNode 身份、列表 Diff、DOM 復用、元件狀態保留與更新策略。

一句話回答:key 是 Vue 用來識別 VNode 身份的提示。它能幫助 Diff 算法判斷哪些節點可以復用、哪些需要新增、刪除或移動,避免列表更新時出現 DOM 或元件狀態錯位。


一、 必考觀念

1. key 是什麼?

在列表渲染中,我們通常會這樣寫:

vue
<li
  v-for="item in list"
  :key="item.id"
>
  {{ item.name }}
</li>

key 告訴 Vue:這個 VNode 對應的是哪一筆資料。

text
VNode key = 1 -> user id 1
VNode key = 2 -> user id 2
VNode key = 3 -> user id 3

當列表順序變化時,Vue 可以透過 key 追蹤節點身份,而不是只看它在陣列中的位置。

2. 沒有 key 會怎樣?

沒有 key 時,Vue 會傾向使用就地復用策略。

例如:

text
old: A B C
new: X A B C

如果沒有 key,Vue 可能會把原本第一個 DOM 節點從 A 更新成 X,第二個從 B 更新成 A,第三個從 C 更新成 B,最後再新增 C。

這在純文字列表可能沒問題,但如果每一項裡有 input、checkbox、子元件內部狀態,就可能出現狀態錯位。

3. key 的核心作用:穩定身份

有 key 時:

text
old: A(id=1) B(id=2) C(id=3)
new: X(id=4) A(id=1) B(id=2) C(id=3)

Vue 可以知道:

  • A 還是 A,只是位置變了。
  • B 還是 B,只是位置變了。
  • C 還是 C,只是位置變了。
  • X 是新節點。

二、 key 和 Diff 的關係

1. Diff 需要判斷節點能不能復用

Vue patch 新舊 VNode 時,會先判斷兩個 VNode 是否是同一類節點。

簡化條件:

typescript
function isSameVNode(oldVNode, newVNode) {
  return oldVNode.type === newVNode.type
    && oldVNode.key === newVNode.key
}

真實實作更複雜,但 key 是非常重要的一部分。

如果 type 相同、key 也相同,Vue 傾向復用舊節點並 patch props/children。

如果 key 不同,即使元素長得像,也會被視為不同節點。

2. Keyed Children

有 key 的列表會走 keyed diff。

Vue 可以建立 key 到 index 的映射:

text
new children:
key 3 -> index 0
key 1 -> index 1
key 2 -> index 2

然後掃描舊節點,看它們在新列表中是否存在:

  • 存在:復用並 patch。
  • 不存在:移除。
  • 新列表多出來:新增。
  • 順序改變:移動。

Vue 3 在處理 keyed children 時,還會使用最長遞增子序列減少 DOM move。

延伸讀:Vue 2.x 與 Vue 3.x 渲染器 Diff 算法

3. key 不只是為了「消除 warning」

很多人把 key 當成 Vue 警告要求的東西:

text
Elements in iteration expect to have 'v-bind:key'

但 key 的真正作用是讓 Vue 正確理解列表節點身份。

警告只是提醒你:這裡可能有列表復用與狀態錯位風險。


三、 為什麼不建議用 index 當 key?

1. index 不穩定

vue
<li
  v-for="(item, index) in list"
  :key="index"
>
  {{ item.name }}
</li>

如果列表只是純展示、永遠不插入、不刪除、不排序,index key 可能暫時沒事。

但一旦中間插入:

text
old:
index 0 -> A
index 1 -> B
index 2 -> C

new:
index 0 -> X
index 1 -> A
index 2 -> B
index 3 -> C

A 的 key 從 0 變成 1,B 的 key 從 1 變成 2,C 的 key 從 2 變成 3。Vue 會以為很多節點身份都變了或被錯誤復用。

2. input 狀態錯位範例

vue
<div
  v-for="(user, index) in users"
  :key="index"
>
  <span>{{ user.name }}</span>
  <input />
</div>

如果使用者在第二列 input 打字,接著你在列表前面插入一筆資料,DOM 可能被就地復用,輸入框內容可能跑到另一筆資料旁邊。

正確做法:

vue
<div
  v-for="user in users"
  :key="user.id"
>
  <span>{{ user.name }}</span>
  <input />
</div>

讓 DOM 和 user id 綁在一起。

3. 什麼時候可以用 index?

可以,但條件很窄:

  • 列表是純靜態展示。
  • 不會插入、刪除、排序。
  • 每一項沒有本地狀態、表單、動畫或子元件狀態。

面試時最好說:「index key 不是絕對不能用,但只適合穩定且無狀態的列表;動態列表應該使用資料本身的唯一 id。」


四、 key 的其他用途

1. 強制重建元件

key 不只用在 v-for。也可以用來強制 Vue 把某個元件視為新節點。

vue
<UserForm :key="userId" :user-id="userId" />

userId 改變時,Vue 會卸載舊的 UserForm,再建立新的 UserForm

適合場景:

  • 切換資料時希望重置表單狀態。
  • 希望重新跑完整生命週期。
  • 第三方套件內部狀態難以手動重置。

但不要濫用。大部分情況可以透過 watch props 或正確重置狀態處理。

2. 觸發 Transition

如果同一個元素只是文字改變,Vue 可能會復用它。

vue
<Transition>
  <span>{{ message }}</span>
</Transition>

如果你希望 message 變化時觸發 enter/leave,可以加 key:

vue
<Transition>
  <span :key="message">{{ message }}</span>
</Transition>

這樣 message 不同時,Vue 會把它視為不同節點,Transition 才能觸發。

3. RouterView 搭配 key

有時同一個路由組件在不同 params 下會被復用:

text
/user/1 -> /user/2

如果你希望每次 params 變化都重建頁面,可以:

vue
<router-view :key="$route.fullPath" />

但這會讓整個頁面重建,成本比較高。很多時候更好的做法是 watch route params 載入新資料。

延伸讀:Vue 路由實現:Hash 路由與 History 路由原理


五、 追問題庫

Q1:key 必須全局唯一嗎?

不需要全局唯一,只需要在同一層兄弟節點中唯一。

vue
<li v-for="item in list" :key="item.id" />

這裡的 key 只需要在這個 v-for 產生的同層 children 裡唯一。

Q2:key 可以用物件嗎?

不建議。

key 應該是穩定的 primitive value,例如 string 或 number。

vue
<!-- 推薦 -->
<li :key="item.id" />

<!-- 不推薦 -->
<li :key="item" />

物件引用不穩定,也不利於理解身份。

Q3:key 變了會發生什麼?

Vue 會把它視為不同節點。

結果通常是:

  • 舊節點 unmount。
  • 新節點 mount。
  • 元件本地狀態重置。
  • DOM 重新建立。

這可以用來強制重建,但也可能造成不必要成本。

Q4:key 相同但元素類型不同會復用嗎?

不會。

key 只是身份提示,不代表類型一定相同。Vue 還會看 type。

vue
<div :key="id" />
<span :key="id" />

即使 key 相同,type 不同,也應該替換。

Q5:為什麼有時候加 key 反而變慢?

key 幫助 Vue 正確追蹤身份,但不代表永遠更少操作。

例如你刻意讓 key 每次 render 都變:

vue
<Component :key="Math.random()" />

Vue 每次都會認為是新節點,導致整個元件重建,當然會變慢。

key 必須穩定,不能每次渲染都產生新值。


六、 實作題

題目:下面列表為什麼不該用 index key?

vue
<template>
  <div
    v-for="(todo, index) in todos"
    :key="index"
  >
    <input v-model="todo.done" type="checkbox" />
    <input v-model="todo.title" />
  </div>
</template>

回答重點:

  1. 這是一個會變動的 todo list。
  2. 每列有 checkbox 和 input,存在 DOM 狀態與表單狀態。
  3. 如果插入、刪除或排序,index 會改變。
  4. Vue 可能錯誤復用 DOM 或元件狀態。
  5. 應該使用 todo 的穩定 id。

修正:

vue
<template>
  <div
    v-for="todo in todos"
    :key="todo.id"
  >
    <input v-model="todo.done" type="checkbox" />
    <input v-model="todo.title" />
  </div>
</template>

七、 資深視角

1. key 是資料建模問題

很多 key 問題不是 Vue 問題,而是資料沒有穩定身份。

如果後端沒有 id,前端就會被迫用 index、title、時間戳或隨機值。這都可能造成後續問題。

好的列表資料應該天然有穩定 id。

2. key 不是越多越好

key 的目標是穩定身份。不要為了「保險」亂加會變動的 key。

例如:

vue
<UserCard :key="JSON.stringify(user)" />

只要 user 任一欄位改變,整個 UserCard 就會重建。這通常不是你想要的。

3. key 影響狀態保留策略

同一個 key:傾向復用,保留 DOM/元件狀態。

不同 key:傾向重建,重置 DOM/元件狀態。

所以 key 不只是性能問題,也是狀態生命週期問題。


總結

問題面試回答重點
key 是什麼VNode 身份標識
key 的作用幫助 Diff 正確復用、移動、新增、刪除節點
為什麼不用 index動態列表中 index 不穩定,容易狀態錯位
key 是否全局唯一不需要,只需同層兄弟唯一
key 改變會怎樣Vue 會視為不同節點,通常會重建
其他用途強制重建元件、觸發 Transition、控制 RouterView 復用

一句完整的面試回答可以是:

key 是 Vue 用來識別 VNode 身份的提示,主要用在列表 Diff 中。當列表新增、刪除、排序時,Vue 會根據 key 判斷哪些節點可以復用,哪些需要移動、刪除或新增。穩定的 key 可以避免 DOM 或元件狀態錯位,尤其是列表項裡有 input、checkbox、子元件本地狀態時。通常應該使用資料本身穩定唯一的 id,不建議在動態列表中使用 index,因為插入或刪除會讓 index 改變,導致錯誤復用。

延伸閱讀