Skip to content

摘要

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 的比较)。

  • 主要步骤(高层次):

    1. 头尾双指针:从两端向中间跳过相同 type+key 的项(O(n))。
    2. 中段映射:为新列表的剩余部分建立 key → 索引 映射,定位旧项在新列表中的位置。
    3. 构建“新索引数组”:按旧列表顺序映射到新列表索引。
    4. 最长递增子序列(LIS):在“新索引数组”上求 LIS,LIS 之外的需要移动或新建,最少 DOM 移动。
    5. 卸载不存在于新列表的旧项挂载新出现的项
  • 结论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):组件实例 不变,只触发 propsrenderwatch 等更新,内部状态保留
  • 重建(换 key):旧实例 销毁,新实例 创建,生命周期完整走一遍,内部状态重置

选择标准:你是要“换皮”还是要“换命”? 换皮 ⇒ 不改 key;换命 ⇒ 改 key


七、与生态配合的注意点

  • 第三方控件(图表、编辑器、地图)常有复杂内部状态,改 key 往往是最稳妥的“清场重来”。
  • 但在高频切换大体量控件场景,优先评估是否可以参数化就地更新以节流开销。

结论

  • key 是 Vue 3 Diff 的身份标签
  • 类型一致 + key 一致 ⇒ 就地更新;类型或 key 不一致 ⇒ 卸载重建。
  • 正确使用 key 能避免列表错位、消除状态串味;在需要“重置组件”的场景,改变 key 是最简单且可靠的办法

如需把上面的强制重建模式落到你的交易面板,只需保证:

vue
<SLTPField :key="`tp-${state.tradeSide}`" ... />
<SLTPField :key="`sl-${state.tradeSide}`" ... />

即可在切换买/卖时,让两个子组件以新方向的配置完全初始化

本站总访问