问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

为什么不推荐在React中使用index作为key

发布网友 发布时间:2024-09-17 10:25

我来回答

1个回答

热心网友 时间:2024-10-01 19:07

我们在开发React项目时,根据规范,对于列表、表格类型的元素,每项需要指定key属性,否则会出现warning报错。正如antdesign的Table组件文档中描述的那样,当不指定key时,可能会出现未知错误。

我们需要尽量给每条数据提供一个key属性,在实际项目中,一般数据是从后台获取的,所以我们可以使用唯一的标识id作为key值。但如果数据没有id属性的话,也不推荐使用index作为key。React官网上也有相关的解释说明,不建议使用index作为key,可以去深入了解一下。本文将讨论不推荐的原因,主要分为2点。

Wedon’trecommendusingindexesforkeysiftheorderofitemsmaychange.Thiscannegativelyimpactperformanceandmaycauseissueswithcomponentstate.CheckoutRobinPokorny’sarticleforanin-depthexplanationonthenegativeimpactsofusinganindexasakey.IfyouchoosenottoassignanexplicitkeytolistitemsthenReactwilldefaulttousingindexesaskeys.

性能

在讨论性能问题前,我们先看一个案例。

functionApp(){const[data,setData]=useState([{name:'Tom',id:'id1'},{name:'Sam',id:'id2'},{name:'Ben',id:'id3'},{name:'Pam',id:'id4'},]);return(<divclassName="App"><ul>{data.map(({name},index)=>(<likey={index}>{name}</li>))}</ul><br/><br/><buttononClick={()=>{constnewData=[...data];newData.shift();setData(newData);}}>删除第一项</button></div>);}

在这个案例中,我们使用map函数进行生成多个li标签,每个标签的key值用index进行绑定。当点击按钮的时候,会将data中的第一项进行删除,触发重新渲染。

从截图中可以看到,确实可以达到我们想要的效果,但是如果我们细究一下React实际渲染更新做的事情,会发现效率不高。

<!--更新前--><likey="0">Tom</li><likey="1">Sam</li><likey="2">Ben</li><likey="3">Pam</li><!--更新后--><likey="0">Sam</li><likey="1">Ben</li><likey="2">Pam</li>

我们知道React更新机制是先比较虚拟DOM,然后通过计算差异,再对真实DOM进行操作。

如上述代码所示,更新前后,index依旧从0开始,React进行逐条比较,发现了2条同样key=0的li标签,然后递归比较内部,发现内部的文字由Tom改为了Sam,因此需要找到key=0的li标签,并进行真实DOM操作将内部的文字改为Sam,此时完成本条数据的更新。然后继续比较key=1、key=2的数据,并进行更新。最后,原本的key=3的li标签在新的虚拟DOM中,已经不存在了,于是执行了DOM删除。

<!--更新前--><likey="id1">Tom</li><likey="id2">Sam</li><likey="id3">Ben</li><likey="id4">Pam</li><!--更新后--><likey="id2">Sam</li><likey="id3">Ben</li><likey="id4">Pam</li>

如果我们使用数据的id作为key,一切就不一样了。React一开始就发现key=id1的数据没有了,就会进行删除的操作。而其他3条数据,在进行比较后,会发现无变化,因此不会产生真实DOM的更新。

小结一下,在这个案例中,我们想要将第一条数据进行删除,触发页面上的元素变化。如果我们使用index作为key,会导致所有的真实DOM都发生变化;如果我们使用id作为key,则可以保证最小代价的更新,效率更高。

更新不符预期

如果只是效率问题,可能不是我们需要优先解决的,但是如果更新也会出错的话,那我们就无法忽视了。这里会列举2个例子来说明该问题。

输入框内容functionApp(){const[data,setData]=useState([{name:'Tom',id:'id1'},{name:'Sam',id:'id2'},{name:'Ben',id:'id3'},{name:'Pam',id:'id4'},]);return(<divclassName="App"><ul>{data.map(({name},index)=>(<likey={index}>{name}<input/></li>))}</ul><br/><br/><buttononClick={()=>{constnewData=[...data];newData.shift();setData(newData);}}>删除第一项</button></div>);}

类似之前的案例,但在遍历数据时,这次我们增加一个输入框的渲染。测试案例时,我们首先在每个输入框内输入各自的内容,然后再点击按钮删除第一项。

可以发现,看起来第一项Tom确实被删除了,但是input标签内的输入值依旧保持为1。

<!--更新前--><likey="0">Tom<input/></li><likey="1">Sam<input/></li><likey="2">Ben<input/></li><likey="3">Pam<input/></li><!--更新后--><likey="0">Sam<input/></li><likey="1">Ben<input/></li><likey="2">Pam<input/></li>

原因也很容易理解,正如之前的例子一样,虽然看起来是第一项被删除了,但实际上,React在计算差异时,最终删除的其实是最后一项。看起来是第一条被删除的原因是,React递归的比较内部的差异,然后更新了文字内容。而从结果上,我们也可以发现input标签在前后比较中,被认为未发生变化,因此输入值依旧保留着我们更新前输入的内容。而最后一项li标签被删除,所以input标签也同时被删除了。

文字标记

有时候,我们会在网页中进行一些标记,类似划词翻译、主体识别等需求,当涉及数据修改的情况,使用index作为key容易出现问题。

functionApp(){const[data,setData]=useState([{name:'Tom',id:'id1'},{name:'Sam',id:'id2'},{name:'Ben',id:'id3'},{name:'Pam',id:'id4'},]);return(<divclassName="App"><ul>{data.map(({name},index)=>(<likey={index}className="my-list-item">{name}<input/></li>))}</ul><br/><buttononClick={()=>{constfirstItem=document.getElementsByClassName('my-list-item')[0];firstItem.innerHTML=firstItem.innerHTML.replace('m','<a>m</a>');}}>标记第一项</button><br/><buttononClick={()=>{constnewData=[...data];newData.shift();setData(newData);}}>删除第一项</button></div>);}

在上述案例中,我们把第一项中的m进行标记,通过增加a标签包裹的形式,此时进行删除第一项会发现严重的更新错误。

我们可以发现第一项并没有被删除,反而是第二项被删除了。

<!--更新前--><likey="0">Tom<input/></li><likey="1">Sam<input/></li><likey="2">Ben<input/></li><likey="3">Pam<input/></li><!--标注后--><likey="0">To<a>m</a><input/></li><likey="1">Sam<input/></li><likey="2">Ben<input/></li><likey="3">Pam<input/></li><!--更新后--><likey="0">Sam<input/></li><likey="1">Ben<input/></li><likey="2">Pam<input/></li>

我们知道React更新的时候会首先比较虚拟DOM,然后计算如何更新,再将更新过程映射成真实DOM的操作,并最终完成更新。标注行为在这个案例中是直接通过修改DOM的形式进行操作的,React并不知道发生了变化,因此在计算虚拟DOM变化的时候,依旧是用的<likey="0">Tom<input/></li>而不是<likey="0">To<a>m</a><input/></li>。在更新前,此处还有个特殊点在于,li标签下的Tom被视为一个隐式的节点,当发生更新的时候,预期的操作是,将该隐式节点替换成Sam。然而经过标注后,节点被破坏了,变成了To和span2个节点。找不到Tom节点导致React无法完成预期的更新操作。

如果希望解决这样的问题,可以试着将input标签去除,此时由于li标签只有一个子节点Tom,因此React可以直接修改li的innerHTML完成更新。或者我们保留input标签,但是给Tom外层增加一个span标签,此时React更新的时候会修改li内第一个标签,即span标签进行更新。

当然最好还是避免使用index作为key!

小结一下,在这两个案例中,发生更新的元素内由于存在无法被判断变化的元素,比如input,或者元素发生了React无法预知的修改导致与虚拟DOM不再匹配。在更新元素时,会出现更新不符预期的情况。

总结

不推荐使用index作为key,除非不涉及任何更新修改

使用index作为key会导致渲染效率的问题

使用index作为key会导致更新不符预期的问题

如果读者有兴趣的话,可以去深度了解一下React的diffing算法。

本文所用代码可在本人仓库找到:gitee

原文:https://juejin.cn/post/7099745927413366797
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
高考560分能上211大学吗? - 知乎 河北高考多少分能上211大学 河北2023高考211分数线是多少? 考560分能上211大学吗河北 刀剑英雄合王者武器多少费用 刀剑英雄帝辰王者现在什么价位 2021年度工程施工合同范本 2021承包转让简单的合同范本 2021医院食堂承包合同范本 div+css+js实现菜单的收缩与展开 调用数据库内容的时候为什么内容字段... 从错误边界到回滚到MWI React component 聊一聊 React 中更新 ui 视图的几种方式 React17新特性:启发式更新算法 英雄联盟55开真名叫什么 想要通过加装一个行车记录仪,实现倒车影像显示,蓝牙音乐播放(可实现U盘... 小米米兔儿童电话手表如何恢复出厂设置 小米手表怎么重置 怎么恢复小米米兔儿童电话手表的出厂设置 电动牙刷需要手动来回刷吗(电动牙刷需要来回刷吗?) ...身份证是在湖南娄底办理的,我现在在长沙,请问可以在当地办理吗?可以... 求送"知己"的祝福话语? ...外贸服装行业,需要祝福语实用,亲切,有文采! ...还有,平时的话,网络上已经很熟悉了,初次见面送个什么呢 员工安全生产责任制 员工的安全生产责任制内容 三星闹钟设置(三星手机怎么调闹钟) 三星闹钟在哪里设置啊? 上海夺畅6500底薪是真实的么啊 唱歌跑调的人要唱什么歌才不容易跑? Batch Update 浅析 ReactFiber调度机制浅析 尝试全解React(一) 淘宝免费开店遇到上传身份证怎么弄 在淘宝网上开店认证使用别人的身份没问题吗? 阑尾炎术后不能吃什么食物 淘宝网开店实名认证时,除身份证还必须要户口本原件扫描或拍摄件吗_百... 剖腹产不能吃什么蔬菜 吃蒜会不会影响刀口的愈合? 尺骨桡骨骨折术后饮食能吃什么 会计出纳属于什么岗位 出纳会计属于什么行业 会计出纳是干什么的 出纳叫什么岗位 理财卖出按什么时间算 理财t 1是什么意思 理财非交易时间可以买吗 奥运会上哪个项目金牌最多 奥运会上哪个项目金牌数量最多 奥运会金牌最多的项目