Vue 中 key 的作用
key 是 Vue 面試裡非常常見的題目。它看起來只是 v-for 裡的一個屬性,但背後其實牽涉到 VNode 身份、列表 Diff、DOM 復用、元件狀態保留與更新策略。
一句話回答:key 是 Vue 用來識別 VNode 身份的提示。它能幫助 Diff 算法判斷哪些節點可以復用、哪些需要新增、刪除或移動,避免列表更新時出現 DOM 或元件狀態錯位。
一、 必考觀念
1. key 是什麼?
在列表渲染中,我們通常會這樣寫:
<li
v-for="item in list"
:key="item.id"
>
{{ item.name }}
</li>key 告訴 Vue:這個 VNode 對應的是哪一筆資料。
VNode key = 1 -> user id 1
VNode key = 2 -> user id 2
VNode key = 3 -> user id 3當列表順序變化時,Vue 可以透過 key 追蹤節點身份,而不是只看它在陣列中的位置。
2. 沒有 key 會怎樣?
沒有 key 時,Vue 會傾向使用就地復用策略。
例如:
old: A B C
new: X A B C如果沒有 key,Vue 可能會把原本第一個 DOM 節點從 A 更新成 X,第二個從 B 更新成 A,第三個從 C 更新成 B,最後再新增 C。
這在純文字列表可能沒問題,但如果每一項裡有 input、checkbox、子元件內部狀態,就可能出現狀態錯位。
3. key 的核心作用:穩定身份
有 key 時:
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 是否是同一類節點。
簡化條件:
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 的映射:
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 警告要求的東西:
Elements in iteration expect to have 'v-bind:key'但 key 的真正作用是讓 Vue 正確理解列表節點身份。
警告只是提醒你:這裡可能有列表復用與狀態錯位風險。
三、 為什麼不建議用 index 當 key?
1. index 不穩定
<li
v-for="(item, index) in list"
:key="index"
>
{{ item.name }}
</li>如果列表只是純展示、永遠不插入、不刪除、不排序,index key 可能暫時沒事。
但一旦中間插入:
old:
index 0 -> A
index 1 -> B
index 2 -> C
new:
index 0 -> X
index 1 -> A
index 2 -> B
index 3 -> CA 的 key 從 0 變成 1,B 的 key 從 1 變成 2,C 的 key 從 2 變成 3。Vue 會以為很多節點身份都變了或被錯誤復用。
2. input 狀態錯位範例
<div
v-for="(user, index) in users"
:key="index"
>
<span>{{ user.name }}</span>
<input />
</div>如果使用者在第二列 input 打字,接著你在列表前面插入一筆資料,DOM 可能被就地復用,輸入框內容可能跑到另一筆資料旁邊。
正確做法:
<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 把某個元件視為新節點。
<UserForm :key="userId" :user-id="userId" />當 userId 改變時,Vue 會卸載舊的 UserForm,再建立新的 UserForm。
適合場景:
- 切換資料時希望重置表單狀態。
- 希望重新跑完整生命週期。
- 第三方套件內部狀態難以手動重置。
但不要濫用。大部分情況可以透過 watch props 或正確重置狀態處理。
2. 觸發 Transition
如果同一個元素只是文字改變,Vue 可能會復用它。
<Transition>
<span>{{ message }}</span>
</Transition>如果你希望 message 變化時觸發 enter/leave,可以加 key:
<Transition>
<span :key="message">{{ message }}</span>
</Transition>這樣 message 不同時,Vue 會把它視為不同節點,Transition 才能觸發。
3. RouterView 搭配 key
有時同一個路由組件在不同 params 下會被復用:
/user/1 -> /user/2如果你希望每次 params 變化都重建頁面,可以:
<router-view :key="$route.fullPath" />但這會讓整個頁面重建,成本比較高。很多時候更好的做法是 watch route params 載入新資料。
延伸讀:Vue 路由實現:Hash 路由與 History 路由原理。
五、 追問題庫
Q1:key 必須全局唯一嗎?
不需要全局唯一,只需要在同一層兄弟節點中唯一。
<li v-for="item in list" :key="item.id" />這裡的 key 只需要在這個 v-for 產生的同層 children 裡唯一。
Q2:key 可以用物件嗎?
不建議。
key 應該是穩定的 primitive value,例如 string 或 number。
<!-- 推薦 -->
<li :key="item.id" />
<!-- 不推薦 -->
<li :key="item" />物件引用不穩定,也不利於理解身份。
Q3:key 變了會發生什麼?
Vue 會把它視為不同節點。
結果通常是:
- 舊節點 unmount。
- 新節點 mount。
- 元件本地狀態重置。
- DOM 重新建立。
這可以用來強制重建,但也可能造成不必要成本。
Q4:key 相同但元素類型不同會復用嗎?
不會。
key 只是身份提示,不代表類型一定相同。Vue 還會看 type。
<div :key="id" />
<span :key="id" />即使 key 相同,type 不同,也應該替換。
Q5:為什麼有時候加 key 反而變慢?
key 幫助 Vue 正確追蹤身份,但不代表永遠更少操作。
例如你刻意讓 key 每次 render 都變:
<Component :key="Math.random()" />Vue 每次都會認為是新節點,導致整個元件重建,當然會變慢。
key 必須穩定,不能每次渲染都產生新值。
六、 實作題
題目:下面列表為什麼不該用 index key?
<template>
<div
v-for="(todo, index) in todos"
:key="index"
>
<input v-model="todo.done" type="checkbox" />
<input v-model="todo.title" />
</div>
</template>回答重點:
- 這是一個會變動的 todo list。
- 每列有 checkbox 和 input,存在 DOM 狀態與表單狀態。
- 如果插入、刪除或排序,index 會改變。
- Vue 可能錯誤復用 DOM 或元件狀態。
- 應該使用 todo 的穩定 id。
修正:
<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。
例如:
<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 改變,導致錯誤復用。