React 中 key 的作用

React 中 key 的作用是什么?

Date: August 31, 2025
Area: 原理


key 概念

在 React 中,key 用于识别哪些元素是变化、添加或删除的。

在列表渲染中,key 尤其重要,因为它能提高渲染性能和确保组件状态的一致性。


key 的作用

1)唯一性标识:

React 通过 key 唯一标识列表中的每个元素。当列表发生变化(增删改排序)时,React 会通过 key 快速判断:

  • 哪些元素是新增的(需要创建新 DOM 节点)
  • 哪些元素是移除的(需要销毁旧 DOM 节点)
  • 哪些元素是移动的(直接复用现有 DOM 节点,仅调整顺序)

如果没有 key,React 会默认使用数组索引(index)作为标识,这在动态列表中会导致 性能下降状态错误

2)保持组件状态:

使用 key 能确保组件在更新过程中状态的一致性。不同的 key 会使 React 认为它们是不同的组件实例,因而会创建新的组件实例,而不是重用现有实例。这对于有状态的组件尤为重要。

// 如果初始列表是 [A, B],用索引 index 作为 key:
<ul>{items.map((item, index) => (<li key={index}>{item}</li>))}
</ul>// 在头部插入新元素变为 [C, A, B] 时:
// React 会认为 key=0 → C(重新创建)
// key=1 → A(复用原 key=0 的 DOM,但状态可能残留)
// 此时,原本属于 A 的输入框状态可能会错误地出现在 C 中。

3)高效的 Diff 算法:

在列表中使用 key 属性,React 可以通过 Diff 算法快速比较新旧元素,确定哪些元素需要重新渲染,哪些元素可以复用。这减少了不必要的 DOM 操作,从而提高渲染性能。


源码解析

以下是 React 源码中与 key 相关的关键部分:

1)生成 Fiber树

在生成 Fiber 树时,React 使用 key 来匹配新旧节点。

src/react/packages/react-reconciler/src/ReactChildFiber.js

  • Code:

        // * 协调子节点,构建新的子fiber结构,并且返回新的子fiberfunction reconcileChildFibers(returnFiber: Fiber,currentFirstChild: Fiber | null, // 老fiber的第一个子节点newChild: any,lanes: Lanes,): Fiber | null {// This indirection only exists so we can reset `thenableState` at the end.// It should get inlined by Closure.thenableIndexCounter = 0;const firstChildFiber = reconcileChildFibersImpl(returnFiber,currentFirstChild,newChild,lanes,null, // debugInfo);thenableState = null;// Don't bother to reset `thenableIndexCounter` to 0 because it always gets// set at the beginning.return firstChildFiber;}function reconcileChildrenArray(returnFiber: Fiber,currentFirstChild: Fiber | null,newChildren: Array<any>,lanes: Lanes,debugInfo: ReactDebugInfo | null,): Fiber | null {let resultingFirstChild: Fiber | null = null; // 存储新生成的childlet previousNewFiber: Fiber | null = null;let oldFiber = currentFirstChild;let lastPlacedIndex = 0;let newIdx = 0;let nextOldFiber = null;// ! 1. 从左边往右遍历,比较新老节点,如果节点可以复用,继续往右,否则就停止for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {if (oldFiber.index > newIdx) {nextOldFiber = oldFiber;oldFiber = null;} else {nextOldFiber = oldFiber.sibling;}const newFiber = updateSlot(returnFiber,oldFiber,newChildren[newIdx],lanes,debugInfo,);if (newFiber === null) {// TODO: This breaks on empty slots like null children. That's// unfortunate because it triggers the slow path all the time. We need// a better way to communicate whether this was a miss or null,// boolean, undefined, etc.if (oldFiber === null) {oldFiber = nextOldFiber;}break;}if (shouldTrackSideEffects) {if (oldFiber && newFiber.alternate === null) {// We matched the slot, but we didn't reuse the existing fiber, so we// need to delete the existing child.deleteChild(returnFiber, oldFiber);}}lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);if (previousNewFiber === null) {// TODO: Move out of the loop. This only happens for the first run.resultingFirstChild = newFiber;} else {// TODO: Defer siblings if we're not at the right index for this slot.// I.e. if we had null values before, then we want to defer this// for each null value. However, we also don't want to call updateSlot// with the previous one.previousNewFiber.sibling = newFiber;}previousNewFiber = newFiber;oldFiber = nextOldFiber;}// !2.1 新节点没了,(老节点还有)。则删除剩余的老节点即可// 0 1 2 3 4// 0 1 2 3if (newIdx === newChildren.length) {// We've reached the end of the new children. We can delete the rest.deleteRemainingChildren(returnFiber, oldFiber);if (getIsHydrating()) {const numberOfForks = newIdx;pushTreeFork(returnFiber, numberOfForks);}return resultingFirstChild;}// ! 2.2 (新节点还有),老节点没了// 0 1 2 3 4// 0 1 2 3 4 5if (oldFiber === null) {// If we don't have any more existing children we can choose a fast path// since the rest will all be insertions.for (; newIdx < newChildren.length; newIdx++) {const newFiber = createChild(returnFiber,newChildren[newIdx],lanes,debugInfo,);if (newFiber === null) {continue;}lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);if (previousNewFiber === null) {// TODO: Move out of the loop. This only happens for the first run.resultingFirstChild = newFiber;} else {previousNewFiber.sibling = newFiber;}previousNewFiber = newFiber;}if (getIsHydrating()) {const numberOfForks = newIdx;pushTreeFork(returnFiber, numberOfForks);}return resultingFirstChild;}// !2.3 新老节点都还有节点,但是因为老fiber是链表,不方便快速get与delete,// !   因此把老fiber链表中的节点放入Map中,后续操作这个Map的get与delete// 0 1|   4 5// 0 1| 7 8 2 3// Add all children to a key map for quick lookups.const existingChildren = mapRemainingChildren(returnFiber, oldFiber);// Keep scanning and use the map to restore deleted items as moves.for (; newIdx < newChildren.length; newIdx++) {const newFiber = updateFromMap(existingChildren,returnFiber,newIdx,newChildren[newIdx],lanes,debugInfo,);if (newFiber !== null) {if (shouldTrackSideEffects) {if (newFiber.alternate !== null) {// The new fiber is a work in progress, but if there exists a// current, that means that we reused the fiber. We need to delete// it from the child list so that we don't add it to the deletion// list.existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key,);}}lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);if (previousNewFiber === null) {resultingFirstChild = newFiber;} else {previousNewFiber.sibling = newFiber;}previousNewFiber = newFiber;}}// !3. 如果是组件更新阶段,此时新节点已经遍历完了,能复用的老节点都用完了,// ! 则最后查找Map里是否还有元素,如果有,则证明是新节点里不能复用的,也就是要被删除的元素,此时删除这些元素就可以了if (shouldTrackSideEffects) {// Any existing children that weren't consumed above were deleted. We need// to add them to the deletion list.existingChildren.forEach(child => deleteChild(returnFiber, child));}if (getIsHydrating()) {const numberOfForks = newIdx;pushTreeFork(returnFiber, numberOfForks);}return resultingFirstChild;}
    

在 reconcileChildFibers 中的关键使用:

顶层“单个元素”分支(如 reconcileSingleElement):先在兄弟链表里按 key 查找可复用的老 Fiber;若 key 相同再比类型,复用成功则删除其他老兄弟,否则删到尾并新建。

  function reconcileSingleElement(returnFiber: Fiber,currentFirstChild: Fiber | null,element: ReactElement,lanes: Lanes,debugInfo: ReactDebugInfo | null,): Fiber {const key = element.key;let child = currentFirstChild;// 检查老的fiber单链表中是否有可以复用的节点while (child !== null) {if (child.key === key) {...if (child.elementType === elementType || ... ) {deleteRemainingChildren(returnFiber, child.sibling);const existing = useFiber(child, element.props);...return existing;}deleteRemainingChildren(returnFiber, child);break;} else {deleteChild(returnFiber, child);}}...}
  • 顶层对 Fragment(无 key)特殊处理:若是未带 key 的顶层 Fragment,会直接把 children 取出来按数组/迭代器逻辑继续走。

2)比较新旧节点

在比较新旧节点时,React 通过 key 来确定节点是否相同:

src/react/packages/react-reconciler/src/ReactChildFiber.js

  • Code:

      function updateSlot(returnFiber: Fiber,oldFiber: Fiber | null,newChild: any,lanes: Lanes,debugInfo: null | ReactDebugInfo,): Fiber | null {// Update the fiber if the keys match, otherwise return null.const key = oldFiber !== null ? oldFiber.key : null;if ((typeof newChild === 'string' && newChild !== '') ||typeof newChild === 'number') {// Text nodes don't have keys. If the previous node is implicitly keyed// we can continue to replace it without aborting even if it is not a text// node.if (key !== null) {return null;}return updateTextNode(returnFiber,oldFiber,'' + newChild,lanes,debugInfo,);}if (typeof newChild === 'object' && newChild !== null) {switch (newChild.$$typeof) {case REACT_ELEMENT_TYPE: {if (newChild.key === key) {return updateElement(returnFiber,oldFiber,newChild,lanes,mergeDebugInfo(debugInfo, newChild._debugInfo),);} else {return null;}}case REACT_PORTAL_TYPE: {if (newChild.key === key) {return updatePortal(returnFiber,oldFiber,newChild,lanes,debugInfo,);} else {return null;}}case REACT_LAZY_TYPE: {const payload = newChild._payload;const init = newChild._init;return updateSlot(returnFiber,oldFiber,init(payload),lanes,mergeDebugInfo(debugInfo, newChild._debugInfo),);}}if (isArray(newChild) || getIteratorFn(newChild)) {if (key !== null) {return null;}return updateFragment(returnFiber,oldFiber,newChild,lanes,null,mergeDebugInfo(debugInfo, newChild._debugInfo),);}// Usable node types//// Unwrap the inner value and recursively call this function again.if (typeof newChild.then === 'function') {const thenable: Thenable<any> = (newChild: any);return updateSlot(returnFiber,oldFiber,unwrapThenable(thenable),lanes,debugInfo,);}if (newChild.$$typeof === REACT_CONTEXT_TYPE) {const context: ReactContext<mixed> = (newChild: any);return updateSlot(returnFiber,oldFiber,readContextDuringReconcilation(returnFiber, context, lanes),lanes,debugInfo,);}throwOnInvalidObjectType(returnFiber, newChild);}if (__DEV__) {if (typeof newChild === 'function') {warnOnFunctionType(returnFiber, newChild);}if (typeof newChild === 'symbol') {warnOnSymbolType(returnFiber, newChild);}}return null;}
    

实际案例

1)简单列表

假设我们有一个简单的列表:

const items = this.state.items.map(item => <li key={item.id}>{ item.text }</li>
)

在上述代码中,每个

  • 元素都有一个唯一的 key。
  • 如果 items 数组发生变化(如添加或删除元素),React将根据 key 来高效地更新DOM:

    • 当一个元素被删除时,React仅删除对应 key 的DOM节点。
    • 当一个元素被添加时,React 仅在相应的位置插入新的DOM节点。
    • 当一个元素被移动时,React 会识别到位置变化并重新排列 DOM 节点。

    2)错误案例演示

    在这里插入图片描述

    import React, { useState } from 'react'// 错误案例:使用数组索引作为 key,导致组件在插入/重排时状态错乱
    // 复现实验:
    // 1) 在下方两个输入框分别输入不同文本(对应 A、B)
    // 2) 点击“在头部插入 C” → 列表从 [A, B] 变为 [C, A, B]
    // 3) 使用 index 作为 key 时:
    //    key=0 → C(重新创建)
    //    key=1 → A(复用原 key=0 的 DOM,状态可能残留)
    //    因此原本属于 A 的输入框状态可能会错误地出现在 C 中function InputItem({ label }: { label: string }) {const [text, setText] = useState<string>('')return (<divstyle={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}><span style={{ width: 80 }}>{label}</span><inputplaceholder="在此输入以观察状态"value={text}onChange={e => setText(e.target.value)}/></div>)
    }export default function TestDemo() {const [labels, setLabels] = useState<string[]>(['A', 'B'])const prependC = () => {setLabels(prev => ['C', ...prev])}return (<div style={{ padding: 16 }}><h3>错误示例:使用 index 作为 key(头部插入触发状态错乱)</h3><button onClick={prependC} style={{ marginBottom: 12 }}>在头部插入 C</button>{labels.map((label, index) => (// 错误:使用 index 作为 key,头部插入 C 后会发生状态错位<InputItem key={index} label={label} />))}</div>)
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/97733.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/97733.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

wpf之附加属性

前言 附加属性是 WPF 中一个非常强大和独特的概念。简单来说&#xff0c;它允许一个对象为另一个在其本身类定义中未定义的属性赋值。 1、定义附加属性 定义一个Watermark的附加属性&#xff0c;该属性的作用是将TextBox的附加属性改变时&#xff0c;TextBox的字体颜色改成灰…

深入浅出 RabbitMQ-消息可靠性投递

大家好&#xff0c;我是工藤学编程 &#x1f989;一个正在努力学习的小博主&#xff0c;期待你的关注实战代码系列最新文章&#x1f609;C实现图书管理系统&#xff08;Qt C GUI界面版&#xff09;SpringBoot实战系列&#x1f437;【SpringBoot实战系列】SpringBoot3.X 整合 Mi…

数字化时代,中小企业如何落地数字化转型

大数据时代&#xff0c;各行各业的行业龙头和大型集团都已经开始了数据管理&#xff0c;让数据成为数据资产。但是在我国&#xff0c;中小企业的数量巨大&#xff0c;很多管理者忽视了这一点&#xff0c;今天我们就来聊一聊中小企业的数字化转型。中小企业需要数字化转型首先要…

Unity笔记(九)——画线功能Linerenderer、范围检测、射线检测

写在前面&#xff1a;写本系列(自用)的目的是回顾已经学过的知识、记录新学习的知识或是记录心得理解&#xff0c;方便自己以后快速复习&#xff0c;减少遗忘。这里只记录代码知识。十一、画线功能Linerenderer画线功能Linerenderer是Unity提供的画线脚本&#xff0c;创建一个空…

刷题记录(8)string类操作使用

一、仅反转字母 917. 仅仅反转字母 - 力扣&#xff08;LeetCode&#xff09; 简单来说输入字符串&#xff0c;要求你返回所有仅字母位置反转后的字符串。 简单看一个样例加深理解&#xff1a; 前后互换&#xff0c;我想思路基本很明显了&#xff0c;双指针&#xff0c;或者说…

用好AI,从提示词工程到上下文工程

前言 随着 AI 大模型的爆发,提示词工程(prompt engineering ) 一度是用户应用 AI ,发挥 AI 能力最重要、也最应该掌握的技术。 但现在,在 “提示词工程”的基础上,一个更宽泛也更强力的演化概念被提出,也就是本文我们要介绍的 “上下文工程(Context Engineering)” …

计算机Python毕业设计推荐:基于Django+Vue用户评论挖掘旅游系统

精彩专栏推荐订阅&#xff1a;在下方主页&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f496;&#x1f525;作者主页&#xff1a;计算机毕设木哥&#x1f525; &#x1f496; 文章目录 一、项目介绍二、…

⸢ 肆 ⸥ ⤳ 默认安全:安全建设方案 ➭ a.信息安全基线

&#x1f44d;点「赞」&#x1f4cc;收「藏」&#x1f440;关「注」&#x1f4ac;评「论」 在金融科技深度融合的背景下&#xff0c;信息安全已从单纯的技术攻防扩展至架构、合规、流程与创新的系统工程。作为一名从业十多年的老兵&#xff0c;将系统阐述数字银行安全体系的建设…

如何用AI视频增强清晰度软件解决画质模糊问题

在视频制作和分享过程中&#xff0c;画质模糊、细节丢失等问题常常影响观看体验。无论是老旧视频的修复还是低分辨率素材的优化&#xff0c;清晰度提升都成为用户关注的重点。借助专业的AI技术&#xff0c;这些问题可以得到有效解决。目前市面上存在多种解决方案&#xff0c;能…

Linux92 shell:倒计时,用户分类

问题 while IFS read -r line;doootweb kk]# tail -6 /etc/passwd user1r4:x:1040:1040::/home/user1r4:/bin/bash useros20:x:1041:1041::/home/useros20:/bin/bash useros21:x:1042:1042::/home/useros21:/bin/bash useros22:x:1043:1043::/home/useros22:/bin/bash useros23…

LinkedList源码解析

1. 数据结构设计 (1) 节点结构 LinkedList 的核心是双向链表节点 Node&#xff1a; private static class Node<E> {E item; // 存储的元素Node<E> next; // 后继节点Node<E> prev; // 前驱节点Node(Node<E> prev, E element, Node<E&g…

语雀批量导出知识库

使用工具&#xff1a;yuque-dl 参考文档&#xff1a; GitHub - gxr404/yuque-dl: yuque 语雀知识库下载 Yuque-DL&#xff1a;一款强大的语雀资源下载工具_语雀文档怎么下载-CSDN博客

电子电气架构 --- 当前企业EEA现状(下)

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…

flink中的窗口的介绍

本文重点 无界流会源源不断的产生数据,有的时候我们需要把无界流进行切分成一段一段的有界数据,把一段内的所有数据看成一个整体进行聚合计算,这是实现无界流转成有界流的方式之一。 为什么需要窗口 数据是源源不断产生的,我们可能只关心某个周期内的统计结果。比如电费…

自建es 通过Flink同步mysql数据 Docker Compose

资源es:7.18 kibana:7.18 flink:1.17.2目录mkdir -p /usr/project/flink/{conf,job,logs} chmod -R 777 /usr/project/flink #资源情况 mysql8.0 Elasticsearch7.18 自建# 目录结构 /usr/project/flink/ /usr/project/flink/ ├── conf/ │ ├── flink-conf.yaml │ └…

AI浏览器和钉钉ONE是不是伪需求?

最近两则新闻格外引起了我的注意&#xff1a;一是Claude推出了官方浏览器插件&#xff0c;二是钉钉发布了钉钉ONE。前者说明AI浏览器未必有必要&#xff0c;后者则描绘了一幅“刷刷手机就能完成工作”的未来办公图景。这几天我经常在思考&#xff0c;AI浏览器是不是没有必要&am…

从结构化到多模态:RAG文档解析工具选型全指南

在RAG系统建设中&#xff0c;文档解析质量直接决定最终效果上限&#xff0c;选择合适的解析工具已成为避免"垃圾进&#xff0c;垃圾出"&#xff08;GIGO&#xff09;困境的关键决策。一、文档解析&#xff1a;RAG系统的基石与瓶颈 当前企业知识库中超过80%的信息存储…

设计模式:享元模式(Flyweight Pattern)

文章目录一、享元模式的介绍二、实例分析三、示例代码一、享元模式的介绍 享元模式&#xff08;Flyweight Pattern&#xff09; 是一种结构型设计模式。通过共享相同对象&#xff0c;减少内存消耗&#xff0c;提高性能。 它摒弃了在每个对象中保存所有数据的方式&#xff0c; 通…

【Go语言入门教程】 Go语言的起源与技术特点:从诞生到现代编程利器(一)

文章目录前言1. Go语言的起源与发展2. Go语言的核心设计团队2.1 Ken Thompson&#xff08;肯汤普森&#xff09;2.2 Rob Pike&#xff08;罗布派克&#xff09;2.3 Robert Griesemer&#xff08;罗伯特格瑞泽默&#xff09;设计动机&#xff1a;解决C的痛点3. Go语言的核心特性…

rocketmq启动与测试

1.更改runserver.sh的内存大小 vi runserver.sh 2.更改 runbroker.sh内存大小 vi runbroker.sh3.设置环境变量 vi ~/.bash_profile 新增 export NAMESRV_ADDRlocalhost:98764.启动 --在bin的上一级目录启动 nohup bin/mqnamesrv & nohup bin/mqbroker &5.查看日志 le…