Appearance
摘要
key 在 Vue 3 里不仅仅用于 v-for。当我们在同一个位置渲染同一种类型的节点/组件时,只要 key 改变,Vue 会把它当作全新的节点处理:旧的被卸载(unmount),新的被创建并挂载(mount)。
一、核心用法速记
- 列表 Diff 标识:
v-for时用:key唯一标识每一项,避免错误复用。 - 强制重建组件:同一位置渲染相同组件,切换
key会触发卸载+新建,从而清空内部状态与副作用。 - 配合
Transition/动画:不同key驱动进入/离场。 - 避免状态串味:表单、第三方控件、复杂内部 state 场景尤佳。
示例(强制重置 SLTPField):
vue
<!-- 复杂逻辑:用 tradeSide 作为 key,切换买/卖将卸载旧实例并创建新实例 -->
<SLTPField
:key="`tp-${state.tradeSide}`"
v-model="state.tpValue"
:config="sltpConfig"
/>二、为什么“换 key 就换生命”——框架底层怎么做的?
2.1 Diff 的入口与“同/不同节点”判定
Vue 3 渲染更新的核心在
patch(oldVNode, newVNode, container, …)。是否可复用的第一步检查:类型 +
key。- 类型相同且
key相同 ⇒ 视为“同一个节点”,执行就地更新(props/children/patch)。 - 类型不同或
key不同 ⇒ 视为“不同节点”,卸载旧的 → 挂载新的。
- 类型相同且
组件也是节点(
type为组件选项/函数),因此组件key变了 = 不是同一个组件实例。
直观理解:Vue 用
(type, key)这对“身份证”给每个 VNode 编号。身份证换了,就不是同一个人了;必须办离职(unmount)+新入职(mount)。
2.2 列表场景的“带 Key Diff”
子节点为数组时,Vue 3 进入
patchKeyedChildren分支(带 Key 的比较)。主要步骤(高层次):
- 头尾双指针:从两端向中间跳过相同
type+key的项(O(n))。 - 中段映射:为新列表的剩余部分建立
key → 索引映射,定位旧项在新列表中的位置。 - 构建“新索引数组”:按旧列表顺序映射到新列表索引。
- 最长递增子序列(LIS):在“新索引数组”上求 LIS,LIS 之外的需要移动或新建,最少 DOM 移动。
- 卸载不存在于新列表的旧项,挂载新出现的项。
- 头尾双指针:从两端向中间跳过相同
结论:
key是列表最小变更的锚点。缺失或错误的key会导致不必要的卸载/复用,出现“错位更新”“状态串味”。
2.3 单节点/同位置的“换 Key 强制重建”
不在列表、而是同一个占位位置渲染相同类型的组件/元素时:
key不同 ⇒patch直接判定不同节点 ⇒ 走完全卸载(销毁副作用、移除 DOM、触发生命周期/指令解绑)⇒ 再创建新实例并挂载(重新跑setup/onMounted等)。
副作用:表单状态、内部
ref、watchers、计时器、第三方实例等全部被清空重来——这正是“重置”的本质。
三、这对我们有什么实际影响?
3.1 想“重置”子组件,就给它一个会变的 key
- 例如方向切换时,
SLTPField的默认区间、步长、校验逻辑可能随side不同。 - 用
:key="state.tradeSide"最简单,无需在子组件里写一堆重置逻辑。
3.2 避免错误复用 & 状态污染
v-for场景必须用稳定且唯一的key(如业务唯一 ID),不要用索引。- 表单行交换、拖拽排序、切页缓存等,
key错了,会出现“值跟着 DOM 走”的诡异问题。
3.3 性能与正确性的权衡
- 频繁更换
key= 频繁销毁/重建,有成本。 - 若只是 props 变化且希望保留内部状态,就不要改
key,让组件就地更新。
四、典型模式与反模式
✅ 推荐
- 列表:
v-for="item in list" :key="item.id" - 强制重建:
<Widget :key="mode" :config="cfg" /> - 动画过渡:
<Transition> <Comp :key="step" /> </Transition>
❌ 避免
- 用数组索引当
key(除非纯静态列表且不会重排)。 - 在需要保留内部状态的组件上滥用变更
key。
五、最小示例
vue
<template>
<div>
<button @click="side = side === 'buy' ? 'sell' : 'buy'">
切换方向:{{ side }}
</button>
<!-- 复杂逻辑:key 绑定 side,触发强制重建以重置内部状态 -->
<PriceEditor :key="side" :side="side" />
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const side = ref<"buy" | "sell">("buy");
</script>PriceEditor内部可能在onMounted里读取side计算初始值、建立订阅等。- 当
side改变时,PriceEditor被卸载并以新 props 新状态重建。
六、和“响应式就地更新”有什么区别?
- 就地更新(同 key):组件实例 不变,只触发
props、render、watch等更新,内部状态保留。 - 重建(换 key):旧实例 销毁,新实例 创建,生命周期完整走一遍,内部状态重置。
选择标准:你是要“换皮”还是要“换命”? 换皮 ⇒ 不改
key;换命 ⇒ 改key。
七、与生态配合的注意点
- 第三方控件(图表、编辑器、地图)常有复杂内部状态,改 key 往往是最稳妥的“清场重来”。
- 但在高频切换或大体量控件场景,优先评估是否可以参数化就地更新以节流开销。
结论
key是 Vue 3 Diff 的身份标签。- 类型一致 +
key一致 ⇒ 就地更新;类型或key不一致 ⇒ 卸载重建。 - 正确使用
key能避免列表错位、消除状态串味;在需要“重置组件”的场景,改变key是最简单且可靠的办法。
如需把上面的强制重建模式落到你的交易面板,只需保证:
vue
<SLTPField :key="`tp-${state.tradeSide}`" ... />
<SLTPField :key="`sl-${state.tradeSide}`" ... />即可在切换买/卖时,让两个子组件以新方向的配置完全初始化。