欢迎光临
我们一直在努力

React列表删减后数据全乱了?这才是你不该用index做key的真相

深入理解 React:为什么不建议使用 index 作为 key?从底层 Diff 算法说起

在 React 开发中,渲染列表是一项极为高频的操作。每当使用 Array.prototype.map 渲染列表时,如果我们忘记给列表项添加 key 属性,控制台必然会抛出那句经典的警告:Warning: Each child in a list should have a unique "key" prop.

为了图省事,很多初学者会直接将 map 回调函数中的 index(数组索引)作为 key 传入。在页面初始渲染时,这似乎没有任何问题。然而,在复杂的业务交互场景下,这个不经意的习惯可能会引发严重的渲染性能问题,甚至是难以排查的组件状态错乱。

本文将从 React 底层的 Diff 算法切入,彻底理清 key 的作用,并推演使用 index 作为 key 引发 Bug 的全过程。

一、 为什么我们需要 key?它的底层作用是什么?

在深入探讨之前,我们需要明确一个前提:操作真实 DOM 是非常昂贵的。为了优化性能,React 引入了虚拟 DOM(Virtual DOM)机制。每次组件状态更新时,React 都会在内存中生成一棵新的虚拟 DOM 树,并与旧的虚拟 DOM 树进行比对(Reconciliation),找出需要局部更新的最小差异。这个寻找差异的过程就是 Diff 算法。

在处理同层级的列表节点时,如果没有一个唯一的标识,React 默认会按照节点的先后顺序进行逐一比对。如果列表的顺序发生了变化(例如在头部插入了一个新节点),React 无法识别出原有节点只是位置发生了移动,从而会机械地将所有后续节点全部销毁并重新创建。

key 就是虚拟 DOM 节点的唯一身份标识。

当列表节点拥有了稳定的 key,React 在进行新旧虚拟 DOM 比对时,就会通过 key 来精确寻找匹配的节点:

  • 发现相同的 key: 如果新旧节点的类型相同,React 会认为这是同一个 DOM 节点,直接复用该真实 DOM,只更新变化的属性。
  • 发现新的 key: 说明有新节点插入,React 会创建新的真实 DOM。
  • 旧的 key 消失: 说明节点被删除,React 会销毁对应的真实 DOM。
  • 借助 key,React 可以将原本复杂度极高的树比对算法优化为 O(n) 的高效操作,最大程度地复用已有 DOM 节点。

    二、 图解灾难现场:用 index 当作 key 会发生什么?

    理解了 key 的本质,我们再来看为什么用 index(数组索引)作为 key 是一种极其危险的做法。

    假设我们有一个待办事项列表,包含三条数据:A、B、C。我们分别看一下使用“数据唯一 ID”和“数组 index”作为 key 时,删除第一条数据(A)的 Diff 过程。

    场景一:正确做法 —— 使用数据的唯一 ID 作为 key

    当使用稳定的唯一标识(如数据库生成的 ID)作为 key 时,节点的对应关系是绝对绑定的。

    #mermaid-svg-iiGb02jpjasCcm1T{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iiGb02jpjasCcm1T .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iiGb02jpjasCcm1T .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iiGb02jpjasCcm1T .error-icon{fill:#552222;}#mermaid-svg-iiGb02jpjasCcm1T .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iiGb02jpjasCcm1T .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iiGb02jpjasCcm1T .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iiGb02jpjasCcm1T .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iiGb02jpjasCcm1T .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iiGb02jpjasCcm1T .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iiGb02jpjasCcm1T .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iiGb02jpjasCcm1T .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iiGb02jpjasCcm1T .marker.cross{stroke:#333333;}#mermaid-svg-iiGb02jpjasCcm1T svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iiGb02jpjasCcm1T p{margin:0;}#mermaid-svg-iiGb02jpjasCcm1T .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-iiGb02jpjasCcm1T .cluster-label text{fill:#333;}#mermaid-svg-iiGb02jpjasCcm1T .cluster-label span{color:#333;}#mermaid-svg-iiGb02jpjasCcm1T .cluster-label span p{background-color:transparent;}#mermaid-svg-iiGb02jpjasCcm1T .label text,#mermaid-svg-iiGb02jpjasCcm1T span{fill:#333;color:#333;}#mermaid-svg-iiGb02jpjasCcm1T .node rect,#mermaid-svg-iiGb02jpjasCcm1T .node circle,#mermaid-svg-iiGb02jpjasCcm1T .node ellipse,#mermaid-svg-iiGb02jpjasCcm1T .node polygon,#mermaid-svg-iiGb02jpjasCcm1T .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iiGb02jpjasCcm1T .rough-node .label text,#mermaid-svg-iiGb02jpjasCcm1T .node .label text,#mermaid-svg-iiGb02jpjasCcm1T .image-shape .label,#mermaid-svg-iiGb02jpjasCcm1T .icon-shape .label{text-anchor:middle;}#mermaid-svg-iiGb02jpjasCcm1T .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iiGb02jpjasCcm1T .rough-node .label,#mermaid-svg-iiGb02jpjasCcm1T .node .label,#mermaid-svg-iiGb02jpjasCcm1T .image-shape .label,#mermaid-svg-iiGb02jpjasCcm1T .icon-shape .label{text-align:center;}#mermaid-svg-iiGb02jpjasCcm1T .node.clickable{cursor:pointer;}#mermaid-svg-iiGb02jpjasCcm1T .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iiGb02jpjasCcm1T .arrowheadPath{fill:#333333;}#mermaid-svg-iiGb02jpjasCcm1T .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iiGb02jpjasCcm1T .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iiGb02jpjasCcm1T .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iiGb02jpjasCcm1T .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iiGb02jpjasCcm1T .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iiGb02jpjasCcm1T .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iiGb02jpjasCcm1T .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iiGb02jpjasCcm1T .cluster text{fill:#333;}#mermaid-svg-iiGb02jpjasCcm1T .cluster span{color:#333;}#mermaid-svg-iiGb02jpjasCcm1T div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iiGb02jpjasCcm1T .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iiGb02jpjasCcm1T rect.text{fill:none;stroke-width:0;}#mermaid-svg-iiGb02jpjasCcm1T .icon-shape,#mermaid-svg-iiGb02jpjasCcm1T .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iiGb02jpjasCcm1T .icon-shape p,#mermaid-svg-iiGb02jpjasCcm1T .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iiGb02jpjasCcm1T .icon-shape rect,#mermaid-svg-iiGb02jpjasCcm1T .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iiGb02jpjasCcm1T .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iiGb02jpjasCcm1T .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iiGb02jpjasCcm1T :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-iiGb02jpjasCcm1T .old>*{fill:#f9f2f4!important;stroke:#dca7a7!important;stroke-width:2px!important;}#mermaid-svg-iiGb02jpjasCcm1T .old span{fill:#f9f2f4!important;stroke:#dca7a7!important;stroke-width:2px!important;}#mermaid-svg-iiGb02jpjasCcm1T .new>*{fill:#e2f0e5!important;stroke:#8fb996!important;stroke-width:2px!important;}#mermaid-svg-iiGb02jpjasCcm1T .new span{fill:#e2f0e5!important;stroke:#8fb996!important;stroke-width:2px!important;}#mermaid-svg-iiGb02jpjasCcm1T .action>*{fill:#fff2cc!important;stroke:#d6b656!important;stroke-width:2px!important;stroke-dasharray:5 5!important;}#mermaid-svg-iiGb02jpjasCcm1T .action span{fill:#fff2cc!important;stroke:#d6b656!important;stroke-width:2px!important;stroke-dasharray:5 5!important;}

    新虚拟 DOM(删除 A 后)

    旧虚拟 DOM(更新前)

    1. key 匹配:直接复用 DOM2. key 匹配:直接复用 DOM3. key 丢失:卸载该 DOM 节点

    key='id-A': 内容 A

    key='id-B': 内容 B

    key='id-C': 内容 C

    key='id-B': 内容 B

    key='id-C': 内容 C

    卸载销毁

    分析: React 发现 key='id-A' 的节点在新列表中不存在了,于是精准地将第一个真实 DOM 节点从页面上移除。而 key='id-B' 和 key='id-C' 的节点依然存在,React 直接复用它们,仅仅调整它们的位置。操作极其精准且高效。

    场景二:错误做法 —— 使用数组 index 作为 key

    现在,我们用 map 的 index 作为 key。数组删除第一项后,后续元素的索引会自动向前递补。这就导致了严重的“身份错乱”。

    #mermaid-svg-9gdHarZNu8Q3wr8d{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9gdHarZNu8Q3wr8d .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9gdHarZNu8Q3wr8d .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9gdHarZNu8Q3wr8d .error-icon{fill:#552222;}#mermaid-svg-9gdHarZNu8Q3wr8d .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9gdHarZNu8Q3wr8d .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9gdHarZNu8Q3wr8d .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9gdHarZNu8Q3wr8d .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9gdHarZNu8Q3wr8d .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9gdHarZNu8Q3wr8d .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9gdHarZNu8Q3wr8d .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9gdHarZNu8Q3wr8d .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9gdHarZNu8Q3wr8d .marker.cross{stroke:#333333;}#mermaid-svg-9gdHarZNu8Q3wr8d svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9gdHarZNu8Q3wr8d p{margin:0;}#mermaid-svg-9gdHarZNu8Q3wr8d .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-9gdHarZNu8Q3wr8d .cluster-label text{fill:#333;}#mermaid-svg-9gdHarZNu8Q3wr8d .cluster-label span{color:#333;}#mermaid-svg-9gdHarZNu8Q3wr8d .cluster-label span p{background-color:transparent;}#mermaid-svg-9gdHarZNu8Q3wr8d .label text,#mermaid-svg-9gdHarZNu8Q3wr8d span{fill:#333;color:#333;}#mermaid-svg-9gdHarZNu8Q3wr8d .node rect,#mermaid-svg-9gdHarZNu8Q3wr8d .node circle,#mermaid-svg-9gdHarZNu8Q3wr8d .node ellipse,#mermaid-svg-9gdHarZNu8Q3wr8d .node polygon,#mermaid-svg-9gdHarZNu8Q3wr8d .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9gdHarZNu8Q3wr8d .rough-node .label text,#mermaid-svg-9gdHarZNu8Q3wr8d .node .label text,#mermaid-svg-9gdHarZNu8Q3wr8d .image-shape .label,#mermaid-svg-9gdHarZNu8Q3wr8d .icon-shape .label{text-anchor:middle;}#mermaid-svg-9gdHarZNu8Q3wr8d .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9gdHarZNu8Q3wr8d .rough-node .label,#mermaid-svg-9gdHarZNu8Q3wr8d .node .label,#mermaid-svg-9gdHarZNu8Q3wr8d .image-shape .label,#mermaid-svg-9gdHarZNu8Q3wr8d .icon-shape .label{text-align:center;}#mermaid-svg-9gdHarZNu8Q3wr8d .node.clickable{cursor:pointer;}#mermaid-svg-9gdHarZNu8Q3wr8d .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9gdHarZNu8Q3wr8d .arrowheadPath{fill:#333333;}#mermaid-svg-9gdHarZNu8Q3wr8d .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9gdHarZNu8Q3wr8d .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9gdHarZNu8Q3wr8d .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9gdHarZNu8Q3wr8d .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9gdHarZNu8Q3wr8d .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9gdHarZNu8Q3wr8d .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9gdHarZNu8Q3wr8d .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9gdHarZNu8Q3wr8d .cluster text{fill:#333;}#mermaid-svg-9gdHarZNu8Q3wr8d .cluster span{color:#333;}#mermaid-svg-9gdHarZNu8Q3wr8d div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9gdHarZNu8Q3wr8d .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9gdHarZNu8Q3wr8d rect.text{fill:none;stroke-width:0;}#mermaid-svg-9gdHarZNu8Q3wr8d .icon-shape,#mermaid-svg-9gdHarZNu8Q3wr8d .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9gdHarZNu8Q3wr8d .icon-shape p,#mermaid-svg-9gdHarZNu8Q3wr8d .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9gdHarZNu8Q3wr8d .icon-shape rect,#mermaid-svg-9gdHarZNu8Q3wr8d .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9gdHarZNu8Q3wr8d .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9gdHarZNu8Q3wr8d .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9gdHarZNu8Q3wr8d :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-9gdHarZNu8Q3wr8d .old>*{fill:#f9f2f4!important;stroke:#dca7a7!important;stroke-width:2px!important;}#mermaid-svg-9gdHarZNu8Q3wr8d .old span{fill:#f9f2f4!important;stroke:#dca7a7!important;stroke-width:2px!important;}#mermaid-svg-9gdHarZNu8Q3wr8d .new>*{fill:#e2f0e5!important;stroke:#8fb996!important;stroke-width:2px!important;}#mermaid-svg-9gdHarZNu8Q3wr8d .new span{fill:#e2f0e5!important;stroke:#8fb996!important;stroke-width:2px!important;}#mermaid-svg-9gdHarZNu8Q3wr8d .warning>*{fill:#f8cecc!important;stroke:#b85450!important;stroke-width:2px!important;stroke-dasharray:5 5!important;}#mermaid-svg-9gdHarZNu8Q3wr8d .warning span{fill:#f8cecc!important;stroke:#b85450!important;stroke-width:2px!important;stroke-dasharray:5 5!important;}

    新虚拟 DOM(删除 A 后,索引重新生成)

    旧虚拟 DOM(以 index 为 key)

    1. key=0 匹配!但内容由 A 变 B:强制更新该 DOM 节点2. key=1 匹配!但内容由 B 变 C:强制更新该 DOM 节点3. key=2 丢失:卸载该 DOM 节点

    key=0: 内容 A

    key=1: 内容 B

    key=2: 内容 C

    key=0: 内容 B

    key=1: 内容 C

    卸载销毁最后一个 DOM

    灾难推演:

  • React 比较新旧节点的 key=0。它认为节点还在(复用了原本属于 A 的 DOM),但是由于传入的内容变成了 B,React 只能执行更新操作,把页面上第一行的文字从 A 修改为 B。
  • React 比较 key=1。同理,它复用了原本属于 B 的 DOM,将内容强制更新为 C。
  • React 寻找 key=2。发现在新列表中找不到,于是将页面上的最后一行(原本属于 C 的 DOM)给删除了。
  • 我们明明只想删除第一个节点,React 却在底层执行了两次 DOM 内容更新,一次 DOM 节点删除。这完全背离了 Diff 算法优化性能的初衷。

    三、 更严重的后果:组件内部状态(State)错乱

    性能损耗或许在小数据量下不易察觉,但使用 index 还会引发致命的业务逻辑 Bug——组件状态张冠李戴。

    假设列表中的每一项不仅展示文本,还包含一个未受控的 <input> 输入框。
    用户在第一行(A)的输入框里输入了 “hello”,在第二行(B)输入了 “world”。
    此时我们通过代码删除了第一行数据(A)。

    如果是用 index 作为 key:
    React 复用了 key=0 的真实 DOM(也就是原本 A 所在的 DOM 节点)。因为 DOM 节点本身没有被销毁,输入框内部的局部状态(输入的 “hello”)被原封不动地保留了下来。
    最终呈现在页面上的结果是:第一行的文本正确变为了 B,但它旁边的输入框里依然显示着属于 A 的 “hello”。数据与视图状态发生了严重的错位。

    四、 总结与最佳实践

    综上所述,key 的本质是提供给 React 底层 Diff 算法的对比依据。

    提炼:

    key 是 React 虚拟 DOM 的唯一标识。在触发重新渲染进行 Diff 算法比对时,React 依赖 key 来快速识别哪些节点是被添加、移动或删除的,以此最大化复用真实 DOM,提升渲染性能。
    如果使用数组的 index 作为 key,一旦执行了插入、删除、排序等破坏原有数组顺序的操作,就会导致 key 与具体数据对象的绑定关系发生错乱。这不仅会让 React 进行多余的 DOM 更新操作(丧失优化性能的目的),还会导致表单输入框等组件的内部状态错误地附着在其他数据上,引发严重的渲染 Bug。

    我们在日常开发中应该遵循什么原则?

  • 首选数据的唯一标识: 始终使用后端返回的数据 ID(如 item.id、uuid、订单号 等)作为 key。
  • 如果没有唯一 ID 怎么办: 如果数据是前端生成的,可以在创建数据对象的初期,使用 nanoid 或时间戳等生成一个唯一的标识挂载在数据上。
  • 何时可以使用 index: 只有当满足以下所有条件时,才可以勉强使用 index:
    • 列表及列表项是纯静态展示的,不会有任何交互。
    • 列表永远不会执行过滤、排序、首尾插入或删除等破坏原有顺序的操作。
    • 列表项内部不包含任何未受控组件(如 <input>)或局部状态。

    彻底搞懂 key 的原理,能够帮助我们在开发复杂列表交互时避开许多难以调试的暗坑。希望这篇文章能帮你打通这个 React 中的经典知识点。

    赞(0)
    未经允许不得转载:171主机测评 » React列表删减后数据全乱了?这才是你不该用index做key的真相
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址